@kindgi/agents 0.0.0-bootstrap.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +131 -2
- package/dist/agent-turn-flow.d.ts +57 -0
- package/dist/agent-turn-flow.d.ts.map +1 -0
- package/dist/agent-turn-flow.js +172 -0
- package/dist/agent-turn-flow.js.map +1 -0
- package/dist/conversation-binding.d.ts +111 -0
- package/dist/conversation-binding.d.ts.map +1 -0
- package/dist/conversation-binding.js +4 -0
- package/dist/conversation-binding.js.map +1 -0
- package/dist/define.d.ts +180 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +361 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/guardrails-gate.d.ts +169 -0
- package/dist/guardrails-gate.d.ts.map +1 -0
- package/dist/guardrails-gate.js +202 -0
- package/dist/guardrails-gate.js.map +1 -0
- package/dist/handlers/budget-check.d.ts +22 -0
- package/dist/handlers/budget-check.d.ts.map +1 -0
- package/dist/handlers/budget-check.js +109 -0
- package/dist/handlers/budget-check.js.map +1 -0
- package/dist/handlers/build-initial-messages.d.ts +17 -0
- package/dist/handlers/build-initial-messages.d.ts.map +1 -0
- package/dist/handlers/build-initial-messages.js +86 -0
- package/dist/handlers/build-initial-messages.js.map +1 -0
- package/dist/handlers/compose-result.d.ts +10 -0
- package/dist/handlers/compose-result.d.ts.map +1 -0
- package/dist/handlers/compose-result.js +79 -0
- package/dist/handlers/compose-result.js.map +1 -0
- package/dist/handlers/constants.d.ts +8 -0
- package/dist/handlers/constants.d.ts.map +1 -0
- package/dist/handlers/constants.js +10 -0
- package/dist/handlers/constants.js.map +1 -0
- package/dist/handlers/context.d.ts +210 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/context.js +4 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/dispatch-tools.d.ts +15 -0
- package/dist/handlers/dispatch-tools.d.ts.map +1 -0
- package/dist/handlers/dispatch-tools.js +511 -0
- package/dist/handlers/dispatch-tools.js.map +1 -0
- package/dist/handlers/errors.d.ts +133 -0
- package/dist/handlers/errors.d.ts.map +1 -0
- package/dist/handlers/errors.js +134 -0
- package/dist/handlers/errors.js.map +1 -0
- package/dist/handlers/evaluate-guardrails.d.ts +14 -0
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -0
- package/dist/handlers/evaluate-guardrails.js +128 -0
- package/dist/handlers/evaluate-guardrails.js.map +1 -0
- package/dist/handlers/final-iteration.d.ts +7 -0
- package/dist/handlers/final-iteration.d.ts.map +1 -0
- package/dist/handlers/final-iteration.js +26 -0
- package/dist/handlers/final-iteration.js.map +1 -0
- package/dist/handlers/index.d.ts +5 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +41 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/model-call.d.ts +13 -0
- package/dist/handlers/model-call.d.ts.map +1 -0
- package/dist/handlers/model-call.js +136 -0
- package/dist/handlers/model-call.js.map +1 -0
- package/dist/handlers/persist-final-message.d.ts +14 -0
- package/dist/handlers/persist-final-message.d.ts.map +1 -0
- package/dist/handlers/persist-final-message.js +54 -0
- package/dist/handlers/persist-final-message.js.map +1 -0
- package/dist/handlers/persist-provenance.d.ts +12 -0
- package/dist/handlers/persist-provenance.d.ts.map +1 -0
- package/dist/handlers/persist-provenance.js +34 -0
- package/dist/handlers/persist-provenance.js.map +1 -0
- package/dist/handlers/persist-user-message.d.ts +15 -0
- package/dist/handlers/persist-user-message.d.ts.map +1 -0
- package/dist/handlers/persist-user-message.js +59 -0
- package/dist/handlers/persist-user-message.js.map +1 -0
- package/dist/handlers/public-types.d.ts +201 -0
- package/dist/handlers/public-types.d.ts.map +1 -0
- package/dist/handlers/public-types.js +4 -0
- package/dist/handlers/public-types.js.map +1 -0
- package/dist/handlers/rehydrate.d.ts +8 -0
- package/dist/handlers/rehydrate.d.ts.map +1 -0
- package/dist/handlers/rehydrate.js +94 -0
- package/dist/handlers/rehydrate.js.map +1 -0
- package/dist/handlers/render-prompt.d.ts +12 -0
- package/dist/handlers/render-prompt.d.ts.map +1 -0
- package/dist/handlers/render-prompt.js +41 -0
- package/dist/handlers/render-prompt.js.map +1 -0
- package/dist/handlers/resolve-tools.d.ts +10 -0
- package/dist/handlers/resolve-tools.d.ts.map +1 -0
- package/dist/handlers/resolve-tools.js +55 -0
- package/dist/handlers/resolve-tools.js.map +1 -0
- package/dist/handlers/result-shape.d.ts +94 -0
- package/dist/handlers/result-shape.d.ts.map +1 -0
- package/dist/handlers/result-shape.js +19 -0
- package/dist/handlers/result-shape.js.map +1 -0
- package/dist/handlers/run-retrievals.d.ts +10 -0
- package/dist/handlers/run-retrievals.d.ts.map +1 -0
- package/dist/handlers/run-retrievals.js +64 -0
- package/dist/handlers/run-retrievals.js.map +1 -0
- package/dist/handlers/run-snapshot.d.ts +4 -0
- package/dist/handlers/run-snapshot.d.ts.map +1 -0
- package/dist/handlers/run-snapshot.js +26 -0
- package/dist/handlers/run-snapshot.js.map +1 -0
- package/dist/handlers/setup.d.ts +25 -0
- package/dist/handlers/setup.d.ts.map +1 -0
- package/dist/handlers/setup.js +231 -0
- package/dist/handlers/setup.js.map +1 -0
- package/dist/handlers/structured-output.d.ts +38 -0
- package/dist/handlers/structured-output.d.ts.map +1 -0
- package/dist/handlers/structured-output.js +89 -0
- package/dist/handlers/structured-output.js.map +1 -0
- package/dist/handlers/tool-errors.d.ts +56 -0
- package/dist/handlers/tool-errors.d.ts.map +1 -0
- package/dist/handlers/tool-errors.js +73 -0
- package/dist/handlers/tool-errors.js.map +1 -0
- package/dist/handlers/tool-hitl.d.ts +45 -0
- package/dist/handlers/tool-hitl.d.ts.map +1 -0
- package/dist/handlers/tool-hitl.js +81 -0
- package/dist/handlers/tool-hitl.js.map +1 -0
- package/dist/handlers/turn-environment.d.ts +26 -0
- package/dist/handlers/turn-environment.d.ts.map +1 -0
- package/dist/handlers/turn-environment.js +154 -0
- package/dist/handlers/turn-environment.js.map +1 -0
- package/dist/hitl-policy.d.ts +45 -0
- package/dist/hitl-policy.d.ts.map +1 -0
- package/dist/hitl-policy.js +74 -0
- package/dist/hitl-policy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/invoke.d.ts +36 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +228 -0
- package/dist/invoke.js.map +1 -0
- package/dist/migrations-dir.d.ts +11 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +14 -0
- package/dist/migrations-dir.js.map +1 -0
- package/dist/project-run-result.d.ts +23 -0
- package/dist/project-run-result.d.ts.map +1 -0
- package/dist/project-run-result.js +116 -0
- package/dist/project-run-result.js.map +1 -0
- package/dist/prompt.d.ts +83 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +119 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provenance-emit.d.ts +44 -0
- package/dist/provenance-emit.d.ts.map +1 -0
- package/dist/provenance-emit.js +51 -0
- package/dist/provenance-emit.js.map +1 -0
- package/dist/registry.d.ts +38 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +125 -0
- package/dist/registry.js.map +1 -0
- package/dist/retrieval.d.ts +47 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +155 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/run-snapshot-binding.d.ts +77 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -0
- package/dist/run-snapshot-binding.js +4 -0
- package/dist/run-snapshot-binding.js.map +1 -0
- package/dist/schema.d.ts +497 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +133 -0
- package/dist/schema.js.map +1 -0
- package/dist/streaming.d.ts +118 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +17 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tenant-policy.d.ts +16 -0
- package/dist/tenant-policy.d.ts.map +1 -0
- package/dist/tenant-policy.js +77 -0
- package/dist/tenant-policy.js.map +1 -0
- package/dist/types.d.ts +435 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/versioning.d.ts +29 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +58 -0
- package/dist/versioning.js.map +1 -0
- package/migrations/0000_sparkling_talkback.sql +18 -0
- package/migrations/0001_tired_warhawk.sql +16 -0
- package/migrations/0002_violet_ezekiel.sql +2 -0
- package/migrations/meta/0000_snapshot.json +172 -0
- package/migrations/meta/0001_snapshot.json +275 -0
- package/migrations/meta/0002_snapshot.json +287 -0
- package/migrations/meta/_journal.json +27 -0
- package/package.json +76 -4
- package/src/agent-turn-flow.ts +183 -0
- package/src/conversation-binding.ts +147 -0
- package/src/define.ts +572 -0
- package/src/errors.ts +80 -0
- package/src/guardrails-gate.ts +342 -0
- package/src/handlers/budget-check.ts +143 -0
- package/src/handlers/build-initial-messages.ts +103 -0
- package/src/handlers/compose-result.ts +90 -0
- package/src/handlers/constants.ts +10 -0
- package/src/handlers/context.ts +226 -0
- package/src/handlers/dispatch-tools.ts +633 -0
- package/src/handlers/errors.ts +282 -0
- package/src/handlers/evaluate-guardrails.ts +153 -0
- package/src/handlers/final-iteration.ts +30 -0
- package/src/handlers/index.ts +63 -0
- package/src/handlers/model-call.ts +151 -0
- package/src/handlers/persist-final-message.ts +67 -0
- package/src/handlers/persist-provenance.ts +39 -0
- package/src/handlers/persist-user-message.ts +70 -0
- package/src/handlers/public-types.ts +209 -0
- package/src/handlers/rehydrate.ts +161 -0
- package/src/handlers/render-prompt.ts +46 -0
- package/src/handlers/resolve-tools.ts +68 -0
- package/src/handlers/result-shape.ts +113 -0
- package/src/handlers/run-retrievals.ts +77 -0
- package/src/handlers/run-snapshot.ts +44 -0
- package/src/handlers/setup.ts +269 -0
- package/src/handlers/structured-output.ts +117 -0
- package/src/handlers/tool-errors.ts +122 -0
- package/src/handlers/tool-hitl.ts +126 -0
- package/src/handlers/turn-environment.ts +191 -0
- package/src/hitl-policy.ts +128 -0
- package/src/index.ts +154 -0
- package/src/invoke.ts +299 -0
- package/src/migrations-dir.ts +17 -0
- package/src/project-run-result.ts +131 -0
- package/src/prompt.ts +185 -0
- package/src/provenance-emit.ts +100 -0
- package/src/registry.ts +164 -0
- package/src/retrieval.ts +219 -0
- package/src/run-snapshot-binding.ts +87 -0
- package/src/schema.ts +154 -0
- package/src/streaming.ts +153 -0
- package/src/tenant-policy.ts +78 -0
- package/src/types.ts +453 -0
- package/src/versioning.ts +77 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Retrying failed tool calls. A failure the turn's policy retries goes
|
|
6
|
+
* back to the model as the call's result — what failed and why — and
|
|
7
|
+
* the turn continues; the model can correct the call. Each retry costs a
|
|
8
|
+
* step, so `budget.maxSteps` still bounds the turn. A failure the policy
|
|
9
|
+
* doesn't retry, or one past `maxRetries`, fails the turn as before.
|
|
10
|
+
*
|
|
11
|
+
* Retries are counted from the turn's messages (the results this module
|
|
12
|
+
* writes carry a marker), so a replayed turn counts the same.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ModelMessage } from '@kindgi/capabilities';
|
|
16
|
+
import { TOOL_ERROR_KINDS, type ToolErrorKind, type ToolErrorsSpec } from '@kindgi/policy-contract';
|
|
17
|
+
|
|
18
|
+
import { isRepairMessage } from './structured-output.js';
|
|
19
|
+
|
|
20
|
+
/** Without an agent setting: one retry, for failures where nothing ran. */
|
|
21
|
+
export const DEFAULT_TOOL_ERRORS: Required<ToolErrorsSpec> = {
|
|
22
|
+
maxRetries: 1,
|
|
23
|
+
retryOn: ['invalid-arguments', 'unknown-tool'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The policy a turn applies. */
|
|
27
|
+
export interface ToolErrorPolicy {
|
|
28
|
+
readonly maxRetries: number;
|
|
29
|
+
readonly retryOn: ReadonlySet<ToolErrorKind>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The agent's setting (or the default), capped by the tenant's
|
|
34
|
+
* `tool-errors` policy: the fewer retries, and only the kinds both
|
|
35
|
+
* allow — like every tenant policy, it can only make the turn stricter.
|
|
36
|
+
*/
|
|
37
|
+
export function effectiveToolErrorPolicy(
|
|
38
|
+
agent: ToolErrorsSpec | undefined,
|
|
39
|
+
tenantCap: ToolErrorsSpec | undefined,
|
|
40
|
+
): ToolErrorPolicy {
|
|
41
|
+
const maxRetries = Math.min(
|
|
42
|
+
agent?.maxRetries ?? DEFAULT_TOOL_ERRORS.maxRetries,
|
|
43
|
+
tenantCap?.maxRetries ?? Number.POSITIVE_INFINITY,
|
|
44
|
+
);
|
|
45
|
+
const allowed = new Set(tenantCap?.retryOn ?? TOOL_ERROR_KINDS);
|
|
46
|
+
const retryOn = (agent?.retryOn ?? DEFAULT_TOOL_ERRORS.retryOn).filter((k) => allowed.has(k));
|
|
47
|
+
return { maxRetries, retryOn: new Set(retryOn) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Which kind of failure a tool call's error is: arguments that failed
|
|
52
|
+
* the input schema, a tool the agent doesn't have, or a tool that ran
|
|
53
|
+
* and failed.
|
|
54
|
+
*/
|
|
55
|
+
export function toolErrorKindOf(error: {
|
|
56
|
+
readonly code: string;
|
|
57
|
+
readonly cause?: unknown;
|
|
58
|
+
}): ToolErrorKind {
|
|
59
|
+
if (error.code === 'unresolved-tool') return 'unknown-tool';
|
|
60
|
+
const inner = (error.cause as { readonly code?: unknown } | undefined)?.code;
|
|
61
|
+
return inner === 'input-validation-failed' ? 'invalid-arguments' : 'tool-error';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Marks the results this module writes, so they can be counted. */
|
|
65
|
+
const RETRY_MARKER = 'tool-error-retry';
|
|
66
|
+
|
|
67
|
+
/** What the model sees for a failed call it may retry. */
|
|
68
|
+
export interface ToolErrorResult {
|
|
69
|
+
readonly kindgi: typeof RETRY_MARKER;
|
|
70
|
+
readonly status: 'failed';
|
|
71
|
+
readonly error: {
|
|
72
|
+
readonly kind: ToolErrorKind;
|
|
73
|
+
readonly message: string;
|
|
74
|
+
readonly issues?: readonly unknown[];
|
|
75
|
+
};
|
|
76
|
+
readonly instruction: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const INSTRUCTIONS: Readonly<Record<ToolErrorKind, string>> = {
|
|
80
|
+
'invalid-arguments': "Fix the arguments to match the tool's input schema and call it again.",
|
|
81
|
+
'unknown-tool': 'Call one of the tools you were given instead.',
|
|
82
|
+
'tool-error': 'The tool failed. Call it again if a retry makes sense, or answer without it.',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function toolErrorResult(
|
|
86
|
+
kind: ToolErrorKind,
|
|
87
|
+
message: string,
|
|
88
|
+
issues: readonly unknown[] | undefined,
|
|
89
|
+
): ToolErrorResult {
|
|
90
|
+
return {
|
|
91
|
+
kindgi: RETRY_MARKER,
|
|
92
|
+
status: 'failed',
|
|
93
|
+
error: { kind, message, ...(issues !== undefined && { issues }) },
|
|
94
|
+
instruction: INSTRUCTIONS[kind],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retries this turn has taken: the marked tool results after the turn's
|
|
100
|
+
* user message. Earlier turns' results come back as history; they don't
|
|
101
|
+
* count.
|
|
102
|
+
*/
|
|
103
|
+
export function toolRetriesSoFar(messages: readonly ModelMessage[]): number {
|
|
104
|
+
let start = 0;
|
|
105
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
106
|
+
const m = messages[i];
|
|
107
|
+
if (m !== undefined && m.role === 'user' && !isRepairMessage(m)) {
|
|
108
|
+
start = i + 1;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return messages.slice(start).filter(isRetryResult).length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isRetryResult(m: ModelMessage): boolean {
|
|
116
|
+
if (m.role !== 'tool') return false;
|
|
117
|
+
try {
|
|
118
|
+
return (JSON.parse(m.content) as { readonly kindgi?: unknown }).kindgi === RETRY_MARKER;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// Tool-level HITL — resolves the effective `ToolHitlMode` for a given
|
|
6
|
+
// (agent, tool) pair. Consumed by `dispatch-tools.ts` to
|
|
7
|
+
// decide whether a tool call parks the run on a kernel waitpoint before
|
|
8
|
+
// dispatch.
|
|
9
|
+
//
|
|
10
|
+
// Resolution order (narrower wins, framework default is fail-open):
|
|
11
|
+
// 1. `agent.conversationPolicy.hitl.tools.overrides[toolId]` — exact match
|
|
12
|
+
// 2. `agent.conversationPolicy.hitl.tools.default` — agent-wide default
|
|
13
|
+
// 3. per-tool default from `Tool.mutating` — safe fallback
|
|
14
|
+
// - `mutating: false` → `never_ask`
|
|
15
|
+
// - `mutating: true` or absent → `ask_on_first_use` (defaults safer)
|
|
16
|
+
//
|
|
17
|
+
// The effective-policy resolver (`hitl-policy.ts`) layers the tenant
|
|
18
|
+
// cap on top.
|
|
19
|
+
//
|
|
20
|
+
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
import type { Tool } from '@kindgi/tools';
|
|
24
|
+
|
|
25
|
+
import type { Agent, ToolHitlMode, ToolHitlRule } from '../types.js';
|
|
26
|
+
|
|
27
|
+
export interface ResolvedToolHitl {
|
|
28
|
+
readonly mode: ToolHitlMode;
|
|
29
|
+
readonly requiredRole: 'standard' | 'senior' | 'admin';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveToolHitl(agent: Agent, tool: Tool): ResolvedToolHitl {
|
|
33
|
+
const hitl = agent.conversationPolicy?.hitl;
|
|
34
|
+
const toolsPolicy = hitl?.tools;
|
|
35
|
+
const defaultRole = hitl?.defaultReviewerRole ?? 'standard';
|
|
36
|
+
|
|
37
|
+
// Opt-in: agents that don't declare `hitl.tools` get no tool-level
|
|
38
|
+
// gates. Matches the design's "fail-open" default — HITL is opt-in
|
|
39
|
+
// at the agent level, not implicit.
|
|
40
|
+
if (toolsPolicy === undefined) {
|
|
41
|
+
return { mode: 'never_ask', requiredRole: defaultRole };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 1. Explicit override for this toolId.
|
|
45
|
+
const rawOverride = toolsPolicy.overrides?.[tool.id as unknown as string];
|
|
46
|
+
if (rawOverride !== undefined) {
|
|
47
|
+
const rule: ToolHitlRule =
|
|
48
|
+
typeof rawOverride === 'string' ? { mode: rawOverride } : rawOverride;
|
|
49
|
+
return {
|
|
50
|
+
mode: rule.mode,
|
|
51
|
+
requiredRole: rule.requiredRole ?? defaultRole,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Agent-wide default.
|
|
56
|
+
if (toolsPolicy.default !== undefined) {
|
|
57
|
+
return { mode: toolsPolicy.default, requiredRole: defaultRole };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Per-tool default from Tool.mutating — only reached when
|
|
61
|
+
// `hitl.tools` is present (opt-in) but neither an override nor a
|
|
62
|
+
// default matches. Read-only tools skip the gate; mutating tools
|
|
63
|
+
// ask on first use.
|
|
64
|
+
const mode: ToolHitlMode = tool.mutating === false ? 'never_ask' : 'ask_on_first_use';
|
|
65
|
+
return { mode, requiredRole: defaultRole };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Deterministic hash of tool arguments — used as the cache key for
|
|
70
|
+
* `ask_on_first_use`. Sorted-key JSON so reordered args don't produce
|
|
71
|
+
* a different hash.
|
|
72
|
+
*/
|
|
73
|
+
export function hashToolArgs(args: unknown): string {
|
|
74
|
+
return createHash('sha256').update(canonicalStringify(args)).digest('hex').slice(0, 40);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function canonicalStringify(value: unknown): string {
|
|
78
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
79
|
+
if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(',')}]`;
|
|
80
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>
|
|
81
|
+
a.localeCompare(b),
|
|
82
|
+
);
|
|
83
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalStringify(v)}`).join(',')}}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deterministic waitpoint token for a tool-call gate. Includes both
|
|
88
|
+
* the model-generated call id (unique per iteration) AND the args hash
|
|
89
|
+
* so kernel flow replay lands on the same token, and a re-issued
|
|
90
|
+
* tool-call in a later iteration gets its own gate.
|
|
91
|
+
*/
|
|
92
|
+
export function computeToolCallWaitToken(input: {
|
|
93
|
+
readonly runId: string;
|
|
94
|
+
readonly callId: string;
|
|
95
|
+
readonly argsHash: string;
|
|
96
|
+
}): string {
|
|
97
|
+
return createHash('sha256')
|
|
98
|
+
.update(`tool-call:${input.runId}:${input.callId}:${input.argsHash}`)
|
|
99
|
+
.digest('hex')
|
|
100
|
+
.slice(0, 40);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shape the tool-level waitpoint resolves to when the reviewer decides.
|
|
105
|
+
* Same shape as session-gate decisions — the approvals-complete route
|
|
106
|
+
* materializes it identically.
|
|
107
|
+
*/
|
|
108
|
+
export interface ToolHitlDecision {
|
|
109
|
+
readonly decided: 'approve' | 'reject';
|
|
110
|
+
readonly rationale?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* In-conversation cache of decisions for `ask_on_first_use`. Persisted
|
|
115
|
+
* on `agent_conversations.metadata.hitlToolDecisions` — a flat map of
|
|
116
|
+
* `${toolId}:${argsHash}` → decision. Read/write goes through the
|
|
117
|
+
* conversation row's metadata via the standard update path.
|
|
118
|
+
*/
|
|
119
|
+
export interface ToolDecisionCacheKey {
|
|
120
|
+
readonly toolId: string;
|
|
121
|
+
readonly argsHash: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function cacheKeyFor(key: ToolDecisionCacheKey): string {
|
|
125
|
+
return `${key.toolId}:${key.argsHash}`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a turn runs against: its conversation, and the guardrails, tools,
|
|
6
|
+
* policies and model it resolves at the start. `setup` resolves them
|
|
7
|
+
* once per turn; a resumed turn resolves them again (see
|
|
8
|
+
* `rehydrateTurnContext`), pinned to the model `setup` routed to, since
|
|
9
|
+
* the kernel doesn't re-run a step that already completed and these live
|
|
10
|
+
* on the in-memory `TurnContext`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { route } from '@kindgi/capabilities';
|
|
14
|
+
import type { TenantPolicy } from '@kindgi/capabilities';
|
|
15
|
+
import type { HitlSpec, ToolErrorsSpec } from '@kindgi/policy-contract';
|
|
16
|
+
|
|
17
|
+
import type { ConversationClosedError } from '../errors.js';
|
|
18
|
+
import { resolveGuardrails } from '../guardrails-gate.js';
|
|
19
|
+
import { type EffectiveHitlPolicy, resolveEffectiveHitlPolicy } from '../hitl-policy.js';
|
|
20
|
+
import { mergeTenantPolicies } from '../tenant-policy.js';
|
|
21
|
+
import type { Conversation } from '../types.js';
|
|
22
|
+
import type { TurnContext } from './context.js';
|
|
23
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
24
|
+
import { resolveTurnTools } from './resolve-tools.js';
|
|
25
|
+
import { type ToolErrorPolicy, effectiveToolErrorPolicy } from './tool-errors.js';
|
|
26
|
+
|
|
27
|
+
/** The conversation the turn runs in: open, and opened with this agent version. */
|
|
28
|
+
export async function loadTurnConversation(ctx: TurnContext): Promise<Conversation> {
|
|
29
|
+
const conv = await ctx.bindings.conversationBinding.getConversation(
|
|
30
|
+
ctx.input.tenantId,
|
|
31
|
+
ctx.input.conversationId,
|
|
32
|
+
);
|
|
33
|
+
if (conv.kind === 'err') throwAgentTurnFailure(conv.error);
|
|
34
|
+
if (conv.value.closedAt !== undefined) {
|
|
35
|
+
const err: ConversationClosedError = {
|
|
36
|
+
code: 'conversation-closed',
|
|
37
|
+
message: `Conversation "${ctx.input.conversationId}" is closed`,
|
|
38
|
+
conversationId: ctx.input.conversationId,
|
|
39
|
+
};
|
|
40
|
+
throwAgentTurnFailure(err);
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
conv.value.agentId !== ctx.input.agent.id ||
|
|
44
|
+
conv.value.agentVersion !== ctx.input.agent.version
|
|
45
|
+
) {
|
|
46
|
+
throwAgentTurnFailure({
|
|
47
|
+
code: 'agent-version-mismatch',
|
|
48
|
+
message: `Conversation opened with ${conv.value.agentId}@${conv.value.agentVersion}; invoked with ${ctx.input.agent.id}@${ctx.input.agent.version}`,
|
|
49
|
+
conversationId: ctx.input.conversationId,
|
|
50
|
+
expectedVersion: conv.value.agentVersion,
|
|
51
|
+
actualVersion: ctx.input.agent.version,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
ctx.conversation = conv.value;
|
|
55
|
+
return conv.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The model a turn was routed to, as `setup` journals it. */
|
|
59
|
+
export interface PinnedRoute {
|
|
60
|
+
readonly providerId: string;
|
|
61
|
+
readonly model: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the turn's guardrails, tools, tenant policy, tool-error policy
|
|
66
|
+
* and model onto `ctx`. With `pinned`, the route is that provider and
|
|
67
|
+
* model, still under the tenant's current policy; one no longer
|
|
68
|
+
* registered or allowed fails the turn.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveTurnEnvironment(
|
|
71
|
+
ctx: TurnContext,
|
|
72
|
+
pinned?: PinnedRoute,
|
|
73
|
+
): Promise<PinnedRoute & { readonly toolCount: number }> {
|
|
74
|
+
const invResolution = resolveGuardrails(ctx.input.agent, ctx.bindings);
|
|
75
|
+
if (invResolution.missing.length > 0) {
|
|
76
|
+
throwAgentTurnFailure({
|
|
77
|
+
code: 'unresolved-guardrail',
|
|
78
|
+
message: `Agent "${ctx.input.agent.id}" references guardrails not in the registry: ${invResolution.missing.join(', ')}`,
|
|
79
|
+
guardrailIds: invResolution.missing,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
ctx.guardrails = invResolution.resolved;
|
|
83
|
+
|
|
84
|
+
// This turn's tools come from the tenant's own registry — never a
|
|
85
|
+
// registry shared across concurrent turns of other tenants.
|
|
86
|
+
const tenantTools = await ctx.bindings.toolRegistry.forTenant(ctx.input.tenantId);
|
|
87
|
+
ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent);
|
|
88
|
+
|
|
89
|
+
const capability = ctx.input.agent.capabilities[0];
|
|
90
|
+
if (capability === undefined) {
|
|
91
|
+
throwAgentTurnFailure({
|
|
92
|
+
code: 'capability-routing-failed',
|
|
93
|
+
message: `Agent "${ctx.input.agent.id}" has no capabilities declared`,
|
|
94
|
+
cause: null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Merge the static tenant policy with the one derived from the
|
|
98
|
+
// policy registry (if wired); the result is at least as strict as
|
|
99
|
+
// each (see `mergeTenantPolicies`).
|
|
100
|
+
const derivedPolicy =
|
|
101
|
+
ctx.bindings.policyRegistry !== undefined
|
|
102
|
+
? await ctx.bindings.policyRegistry.evaluate<
|
|
103
|
+
{ readonly tenantId: typeof ctx.input.tenantId },
|
|
104
|
+
TenantPolicy | undefined
|
|
105
|
+
>('model-routing', { tenantId: ctx.input.tenantId })
|
|
106
|
+
: undefined;
|
|
107
|
+
const effectivePolicy = mergeTenantPolicies(ctx.bindings.tenantPolicy, derivedPolicy);
|
|
108
|
+
if (effectivePolicy !== undefined) ctx.tenantPolicy = effectivePolicy;
|
|
109
|
+
ctx.toolErrorPolicy = await resolveToolErrorPolicy(ctx);
|
|
110
|
+
// Storage-backed registries need an async hydration
|
|
111
|
+
// step before the sync `list(tenantId)` call — they read persisted
|
|
112
|
+
// providers from `ProviderRegistryBinding` and instantiate each via
|
|
113
|
+
// its adapter factory. In-memory implementations (dev-echo,
|
|
114
|
+
// tests) leave `hydrate` undefined and this is a no-op.
|
|
115
|
+
if (ctx.bindings.providerRegistry.hydrate !== undefined) {
|
|
116
|
+
await ctx.bindings.providerRegistry.hydrate(ctx.input.tenantId);
|
|
117
|
+
}
|
|
118
|
+
// A pinned route narrows the policy to exactly that provider and model.
|
|
119
|
+
const routingPolicy =
|
|
120
|
+
pinned === undefined
|
|
121
|
+
? effectivePolicy
|
|
122
|
+
: mergeTenantPolicies(effectivePolicy, {
|
|
123
|
+
tenantId: ctx.input.tenantId,
|
|
124
|
+
providers: { allow: [pinned.providerId] },
|
|
125
|
+
models: { allow: [pinned.model] },
|
|
126
|
+
});
|
|
127
|
+
const routed = route({
|
|
128
|
+
capability,
|
|
129
|
+
providers: ctx.bindings.providerRegistry.list(ctx.input.tenantId),
|
|
130
|
+
...(routingPolicy !== undefined && { tenantPolicy: routingPolicy }),
|
|
131
|
+
...(ctx.input.agent.preferredProvider !== undefined && {
|
|
132
|
+
preferredProvider: ctx.input.agent.preferredProvider,
|
|
133
|
+
}),
|
|
134
|
+
...(ctx.input.agent.preferredModel !== undefined && {
|
|
135
|
+
preferredModel: ctx.input.agent.preferredModel,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (routed.kind === 'err') {
|
|
139
|
+
throwAgentTurnFailure({
|
|
140
|
+
code: 'capability-routing-failed',
|
|
141
|
+
message:
|
|
142
|
+
pinned === undefined
|
|
143
|
+
? routed.error.message
|
|
144
|
+
: `The turn was routed to ${pinned.providerId}/${pinned.model}, which is no longer registered or allowed: ${routed.error.message}`,
|
|
145
|
+
cause: routed.error,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
ctx.provider = routed.value.provider;
|
|
149
|
+
ctx.model = routed.value.model;
|
|
150
|
+
return {
|
|
151
|
+
providerId: routed.value.provider.metadata.id,
|
|
152
|
+
model: routed.value.model.name,
|
|
153
|
+
toolCount: ctx.tools.definitions.length,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The turn's approval rules: the agent's, held to the tenant's `hitl`
|
|
159
|
+
* policy. A policy that can't be evaluated fails the turn — running
|
|
160
|
+
* without it would skip the tenant's approvals.
|
|
161
|
+
*/
|
|
162
|
+
export async function resolveTurnHitlPolicy(ctx: TurnContext): Promise<EffectiveHitlPolicy> {
|
|
163
|
+
let tenant: HitlSpec | undefined;
|
|
164
|
+
if (ctx.bindings.policyRegistry !== undefined) {
|
|
165
|
+
try {
|
|
166
|
+
tenant = await ctx.bindings.policyRegistry.evaluate<
|
|
167
|
+
{ readonly tenantId: typeof ctx.input.tenantId },
|
|
168
|
+
HitlSpec | undefined
|
|
169
|
+
>('hitl', { tenantId: ctx.input.tenantId });
|
|
170
|
+
} catch (cause) {
|
|
171
|
+
throwAgentTurnFailure({
|
|
172
|
+
code: 'tenant-policy-unavailable',
|
|
173
|
+
message: `The tenant's hitl policy could not be applied: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
174
|
+
policyKind: 'hitl',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return resolveEffectiveHitlPolicy({ tenant, agent: ctx.input.agent });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The agent's `toolErrors`, capped by the tenant's `tool-errors` policy. */
|
|
182
|
+
async function resolveToolErrorPolicy(ctx: TurnContext): Promise<ToolErrorPolicy> {
|
|
183
|
+
const cap =
|
|
184
|
+
ctx.bindings.policyRegistry !== undefined
|
|
185
|
+
? await ctx.bindings.policyRegistry.evaluate<
|
|
186
|
+
{ readonly tenantId: typeof ctx.input.tenantId },
|
|
187
|
+
ToolErrorsSpec | undefined
|
|
188
|
+
>('tool-errors', { tenantId: ctx.input.tenantId })
|
|
189
|
+
: undefined;
|
|
190
|
+
return effectiveToolErrorPolicy(ctx.input.agent.toolErrors, cap);
|
|
191
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// Effective HITL policy resolver — computes the runtime policy that
|
|
6
|
+
// gates + timeouts consult, merging in this order:
|
|
7
|
+
//
|
|
8
|
+
// 1. Framework defaults (session gate off, no tool gates, 24h timeout,
|
|
9
|
+
// standard reviewer, `escalate` on timeout).
|
|
10
|
+
// 2. Agent's `conversationPolicy.hitl` (and `hitlAfterTurns`).
|
|
11
|
+
// 3. The tenant's `hitl` policy (`HitlSpec`) — only stricter: it can
|
|
12
|
+
// shorten the timeout, raise the reviewer role, and set a floor
|
|
13
|
+
// under a tool's gate, never loosen.
|
|
14
|
+
//
|
|
15
|
+
// The agent turn resolves it once, at the start (see
|
|
16
|
+
// `resolveTurnHitlPolicy`), instead of reading
|
|
17
|
+
// `agent.conversationPolicy.hitl` directly.
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
import { type HitlSpec, higherRole, toolHitlRule } from '@kindgi/policy-contract';
|
|
21
|
+
|
|
22
|
+
import type { Agent, ConversationHitlPolicy, ToolHitlMode, ToolHitlRule } from './types.js';
|
|
23
|
+
|
|
24
|
+
export interface EffectiveHitlPolicy {
|
|
25
|
+
/**
|
|
26
|
+
* Session-turn count gate. Absent = no session gate.
|
|
27
|
+
* Merged from `agent.conversationPolicy.hitl.afterTurns` and
|
|
28
|
+
* `agent.conversationPolicy.hitlAfterTurns` (either fires;
|
|
29
|
+
* `hitl.afterTurns` wins when both are set).
|
|
30
|
+
*/
|
|
31
|
+
readonly turn?: { readonly afterTurns: number };
|
|
32
|
+
/**
|
|
33
|
+
* Tool-level policy. Absent = no tool gates (agents opt in
|
|
34
|
+
* explicitly). Same shape as `agent.conversationPolicy.hitl.tools`,
|
|
35
|
+
* with string overrides normalized to rules and tenant overrides
|
|
36
|
+
* applied; the per-tool default (from `Tool.mutating`) is applied at
|
|
37
|
+
* dispatch time.
|
|
38
|
+
*/
|
|
39
|
+
readonly tools?: {
|
|
40
|
+
readonly default?: ToolHitlMode;
|
|
41
|
+
readonly overrides: ReadonlyMap<string, ToolHitlRule>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The tenant's per-tool rules: a floor under the agent's gate for
|
|
45
|
+
* that tool. A tool's gate is the stricter of the two; tools the
|
|
46
|
+
* tenant names nothing for keep the agent's.
|
|
47
|
+
*/
|
|
48
|
+
readonly toolFloors?: ReadonlyMap<string, ToolHitlRule>;
|
|
49
|
+
readonly defaultReviewerRole: 'standard' | 'senior' | 'admin';
|
|
50
|
+
/** Millisecond timeout used at enqueue time. */
|
|
51
|
+
readonly timeoutMs: number;
|
|
52
|
+
readonly onTimeout: 'auto-approve' | 'auto-reject' | 'escalate';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Framework defaults. Every field non-optional so the resolver's
|
|
57
|
+
* return type is fully-populated regardless of caller policy state.
|
|
58
|
+
*/
|
|
59
|
+
const FRAMEWORK_DEFAULTS = {
|
|
60
|
+
defaultReviewerRole: 'standard' as const,
|
|
61
|
+
timeoutMs: 24 * 60 * 60 * 1000,
|
|
62
|
+
onTimeout: 'escalate' as const,
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute the effective HITL policy for a given (tenant, agent) pair:
|
|
67
|
+
* framework defaults overlaid with the agent's policy, then held to the
|
|
68
|
+
* tenant's `hitl` policy (only stricter). `tenant: undefined` — the
|
|
69
|
+
* tenant has none.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveEffectiveHitlPolicy(input: {
|
|
72
|
+
readonly tenant: HitlSpec | undefined;
|
|
73
|
+
readonly agent: Agent;
|
|
74
|
+
}): EffectiveHitlPolicy {
|
|
75
|
+
const agentHitl: ConversationHitlPolicy | undefined = input.agent.conversationPolicy?.hitl;
|
|
76
|
+
const legacyAfterTurns = input.agent.conversationPolicy?.hitlAfterTurns;
|
|
77
|
+
const agentAfterTurns = agentHitl?.afterTurns ?? legacyAfterTurns;
|
|
78
|
+
|
|
79
|
+
const merged: {
|
|
80
|
+
turn?: { afterTurns: number };
|
|
81
|
+
tools?: {
|
|
82
|
+
default?: ToolHitlMode;
|
|
83
|
+
overrides: ReadonlyMap<string, ToolHitlRule>;
|
|
84
|
+
};
|
|
85
|
+
toolFloors?: ReadonlyMap<string, ToolHitlRule>;
|
|
86
|
+
defaultReviewerRole: 'standard' | 'senior' | 'admin';
|
|
87
|
+
timeoutMs: number;
|
|
88
|
+
onTimeout: 'auto-approve' | 'auto-reject' | 'escalate';
|
|
89
|
+
} = {
|
|
90
|
+
defaultReviewerRole: agentHitl?.defaultReviewerRole ?? FRAMEWORK_DEFAULTS.defaultReviewerRole,
|
|
91
|
+
timeoutMs: agentHitl?.timeoutMs ?? FRAMEWORK_DEFAULTS.timeoutMs,
|
|
92
|
+
onTimeout: FRAMEWORK_DEFAULTS.onTimeout,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (agentAfterTurns !== undefined) {
|
|
96
|
+
merged.turn = { afterTurns: agentAfterTurns };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const agentToolsPolicy = agentHitl?.tools;
|
|
100
|
+
if (agentToolsPolicy !== undefined) {
|
|
101
|
+
const overrides = new Map<string, ToolHitlRule>();
|
|
102
|
+
if (agentToolsPolicy.overrides !== undefined) {
|
|
103
|
+
for (const [k, v] of Object.entries(agentToolsPolicy.overrides)) {
|
|
104
|
+
overrides.set(k, typeof v === 'string' ? { mode: v } : v);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
merged.tools = {
|
|
108
|
+
...(agentToolsPolicy.default !== undefined && { default: agentToolsPolicy.default }),
|
|
109
|
+
overrides,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The tenant's policy — only stricter.
|
|
114
|
+
const tenant = input.tenant;
|
|
115
|
+
if (tenant !== undefined) {
|
|
116
|
+
if (tenant.maxTimeoutMs !== undefined && merged.timeoutMs > tenant.maxTimeoutMs) {
|
|
117
|
+
merged.timeoutMs = tenant.maxTimeoutMs;
|
|
118
|
+
}
|
|
119
|
+
merged.defaultReviewerRole =
|
|
120
|
+
higherRole(merged.defaultReviewerRole, tenant.minReviewerRole) ?? merged.defaultReviewerRole;
|
|
121
|
+
const floors = Object.entries(tenant.tools ?? {});
|
|
122
|
+
if (floors.length > 0) {
|
|
123
|
+
merged.toolFloors = new Map(floors.map(([toolId, value]) => [toolId, toolHitlRule(value)]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return merged as EffectiveHitlPolicy;
|
|
128
|
+
}
|