@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,633 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ModelMessage, ModelToolCall } from '@kindgi/capabilities';
|
|
5
|
+
import type { NodeContext, NodeHandler } from '@kindgi/handler';
|
|
6
|
+
import { WaitpointCancelledError } from '@kindgi/handler';
|
|
7
|
+
import { stricterToolHitlRule } from '@kindgi/policy-contract';
|
|
8
|
+
import { invokeTool } from '@kindgi/tools';
|
|
9
|
+
import type { Tool, ToolContext } from '@kindgi/tools';
|
|
10
|
+
|
|
11
|
+
import { emitTurnEvent } from '../streaming.js';
|
|
12
|
+
import type { ConversationMessage, ToolHitlMode, ToolHitlRule } from '../types.js';
|
|
13
|
+
|
|
14
|
+
import type { EffectiveHitlPolicy } from '../hitl-policy.js';
|
|
15
|
+
|
|
16
|
+
import type { AgentTurnIterationOutput, TurnContext } from './context.js';
|
|
17
|
+
import {
|
|
18
|
+
type ToolInvocationError,
|
|
19
|
+
type UnresolvedToolError,
|
|
20
|
+
throwAgentTurnFailure,
|
|
21
|
+
} from './errors.js';
|
|
22
|
+
import {
|
|
23
|
+
effectiveToolErrorPolicy,
|
|
24
|
+
toolErrorKindOf,
|
|
25
|
+
toolErrorResult,
|
|
26
|
+
toolRetriesSoFar,
|
|
27
|
+
} from './tool-errors.js';
|
|
28
|
+
import { type ToolHitlDecision, computeToolCallWaitToken, hashToolArgs } from './tool-hitl.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the effective HITL mode + reviewer role for a specific
|
|
32
|
+
* tool from the pre-resolved effective policy. Mirrors the standalone
|
|
33
|
+
* `resolveToolHitl(agent, tool)` in tool-hitl.ts but consumes the
|
|
34
|
+
* resolver's output.
|
|
35
|
+
*/
|
|
36
|
+
function resolveEffectiveToolHitl(
|
|
37
|
+
effective: EffectiveHitlPolicy,
|
|
38
|
+
tool: Tool,
|
|
39
|
+
): { readonly mode: ToolHitlMode; readonly requiredRole: 'standard' | 'senior' | 'admin' } {
|
|
40
|
+
const gate = agentToolHitl(effective, tool);
|
|
41
|
+
// The tenant's floor for this tool, when it names one: the stricter wins.
|
|
42
|
+
const floor = effective.toolFloors?.get(tool.id as unknown as string);
|
|
43
|
+
if (floor === undefined) return gate;
|
|
44
|
+
const stricter = stricterToolHitlRule(gate, floor);
|
|
45
|
+
return { mode: stricter.mode, requiredRole: stricter.requiredRole ?? gate.requiredRole };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The agent's own gate for `tool`. */
|
|
49
|
+
function agentToolHitl(
|
|
50
|
+
effective: EffectiveHitlPolicy,
|
|
51
|
+
tool: Tool,
|
|
52
|
+
): { readonly mode: ToolHitlMode; readonly requiredRole: 'standard' | 'senior' | 'admin' } {
|
|
53
|
+
const toolsPolicy = effective.tools;
|
|
54
|
+
const defaultRole = effective.defaultReviewerRole;
|
|
55
|
+
|
|
56
|
+
// Opt-in: no `hitl.tools` block → never gate.
|
|
57
|
+
if (toolsPolicy === undefined) {
|
|
58
|
+
return { mode: 'never_ask', requiredRole: defaultRole };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rawOverride = toolsPolicy.overrides.get(tool.id as unknown as string);
|
|
62
|
+
if (rawOverride !== undefined) {
|
|
63
|
+
const rule: ToolHitlRule =
|
|
64
|
+
typeof rawOverride === 'string'
|
|
65
|
+
? { mode: rawOverride as ToolHitlMode }
|
|
66
|
+
: (rawOverride as ToolHitlRule);
|
|
67
|
+
return {
|
|
68
|
+
mode: rule.mode,
|
|
69
|
+
requiredRole: rule.requiredRole ?? defaultRole,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (toolsPolicy.default !== undefined) {
|
|
74
|
+
return { mode: toolsPolicy.default, requiredRole: defaultRole };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Per-tool default from Tool.mutating (opt-in scope only).
|
|
78
|
+
const mode: ToolHitlMode = tool.mutating === false ? 'never_ask' : 'ask_on_first_use';
|
|
79
|
+
return { mode, requiredRole: defaultRole };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Loop-body node #2. If the model returned `finishReason='tool-use'`
|
|
84
|
+
* with a non-empty tool-call list, persist the assistant tool-call
|
|
85
|
+
* message + dispatch every tool + persist every tool result. Update
|
|
86
|
+
* `nextMessages` so the next iteration's model call sees the tool
|
|
87
|
+
* outputs.
|
|
88
|
+
*
|
|
89
|
+
* When the model returned a terminal `finishReason` (or `tool-use`
|
|
90
|
+
* with no calls), pass through unchanged so
|
|
91
|
+
* `budget-check` marks the iteration finished.
|
|
92
|
+
*/
|
|
93
|
+
export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
94
|
+
return async (input: unknown, kctx: NodeContext) => {
|
|
95
|
+
if (ctx.tools === undefined) {
|
|
96
|
+
throwAgentTurnFailure({
|
|
97
|
+
code: 'model-invocation-failed',
|
|
98
|
+
message: 'dispatch-tools invoked before setup completed',
|
|
99
|
+
cause: null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const partial = input as {
|
|
103
|
+
readonly step: number;
|
|
104
|
+
readonly finishReason: AgentTurnIterationOutput['finishReason'];
|
|
105
|
+
readonly message: ModelMessage;
|
|
106
|
+
readonly iterationUsage: AgentTurnIterationOutput['iterationUsage'];
|
|
107
|
+
readonly provider: AgentTurnIterationOutput['provider'];
|
|
108
|
+
readonly nextMessages: readonly ModelMessage[];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const assistantMsg = partial.message;
|
|
112
|
+
const hasToolCalls =
|
|
113
|
+
partial.finishReason === 'tool-use' &&
|
|
114
|
+
assistantMsg.toolCalls !== undefined &&
|
|
115
|
+
assistantMsg.toolCalls.length > 0;
|
|
116
|
+
|
|
117
|
+
if (!hasToolCalls) {
|
|
118
|
+
// No tool calls this iteration — pass through. `budget-check`
|
|
119
|
+
// will finalize with `finishedTurn=true` and the loop exits.
|
|
120
|
+
const forwarded: Partial<AgentTurnIterationOutput> & {
|
|
121
|
+
readonly step: number;
|
|
122
|
+
readonly hasToolCalls: false;
|
|
123
|
+
} = {
|
|
124
|
+
step: partial.step,
|
|
125
|
+
finishReason: partial.finishReason,
|
|
126
|
+
message: assistantMsg,
|
|
127
|
+
iterationAppended: [],
|
|
128
|
+
iterationUsage: partial.iterationUsage,
|
|
129
|
+
provider: partial.provider,
|
|
130
|
+
nextMessages: partial.nextMessages,
|
|
131
|
+
hasToolCalls: false,
|
|
132
|
+
};
|
|
133
|
+
return forwarded;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const iterationAppended: ConversationMessage[] = [
|
|
137
|
+
await persistToolCallMessage(ctx, assistantMsg, partial.step),
|
|
138
|
+
];
|
|
139
|
+
let nextMessages: ModelMessage[] = [...partial.nextMessages, assistantMsg];
|
|
140
|
+
|
|
141
|
+
for (const call of assistantMsg.toolCalls ?? []) {
|
|
142
|
+
// Resumed after an approval further down this list: a call that
|
|
143
|
+
// already ran keeps the result it stored.
|
|
144
|
+
const stored = takeStoredBeforePark(
|
|
145
|
+
ctx,
|
|
146
|
+
(m) => m.role === 'tool' && m.toolCall?.invocationId === call.id,
|
|
147
|
+
);
|
|
148
|
+
if (stored !== undefined) {
|
|
149
|
+
iterationAppended.push(stored);
|
|
150
|
+
nextMessages = [...nextMessages, toolMessageOf(stored, call.id)];
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Look up the resolved binding before emitting `tool.started` so
|
|
154
|
+
// the event can carry `toolVersion` + `toolVersionRange` alongside
|
|
155
|
+
// the id. Model-requested tools the
|
|
156
|
+
// agent didn't declare emit a bare `tool.failed` with no version.
|
|
157
|
+
const binding = ctx.tools.byName.get(call.name);
|
|
158
|
+
if (binding === undefined) {
|
|
159
|
+
const unresolved: UnresolvedToolError = {
|
|
160
|
+
code: 'unresolved-tool',
|
|
161
|
+
message: `Model requested tool "${call.name}" not registered for agent "${ctx.input.agent.id}"`,
|
|
162
|
+
toolId: call.name,
|
|
163
|
+
};
|
|
164
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
165
|
+
kind: 'tool.failed',
|
|
166
|
+
step: partial.step,
|
|
167
|
+
toolId: call.name,
|
|
168
|
+
invocationId: call.id,
|
|
169
|
+
error: { code: unresolved.code, message: unresolved.message },
|
|
170
|
+
});
|
|
171
|
+
nextMessages = await retryOrFail(ctx, call, unresolved, undefined, {
|
|
172
|
+
toolId: call.name,
|
|
173
|
+
nextMessages,
|
|
174
|
+
iterationAppended,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const { tool, resolvedVersion, requestedRange } = binding;
|
|
179
|
+
|
|
180
|
+
// Tool-level HITL gate. Resolves the effective mode
|
|
181
|
+
// for this (agent, tool) pair via the effective-policy
|
|
182
|
+
// resolver (framework defaults → agent → tenant cap), then parks +
|
|
183
|
+
// enqueues an approval when required BEFORE dispatch. On reject,
|
|
184
|
+
// returns a synthetic negative tool result so the LLM can adapt
|
|
185
|
+
// without terminating the run. Same run, same conversation, same
|
|
186
|
+
// provenance record across the park-and-resume — the same
|
|
187
|
+
// framework guarantee as the session-level gate.
|
|
188
|
+
//
|
|
189
|
+
// Kernel replay: within one run, waitForToken with the same
|
|
190
|
+
// deterministic tokenId returns the resolved decision from the
|
|
191
|
+
// journal — no re-park. Cross-turn `ask_on_first_use` caching
|
|
192
|
+
// (via conversation metadata) is not implemented.
|
|
193
|
+
const effectiveHitl = ctx.hitlPolicy;
|
|
194
|
+
if (effectiveHitl === undefined) {
|
|
195
|
+
throwAgentTurnFailure({
|
|
196
|
+
code: 'model-invocation-failed',
|
|
197
|
+
message: 'dispatch-tools invoked before the turn resolved its approval rules',
|
|
198
|
+
cause: null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const resolvedHitl = resolveEffectiveToolHitl(effectiveHitl, tool);
|
|
202
|
+
let toolRejectionPayload: { readonly rationale?: string } | null = null;
|
|
203
|
+
if (resolvedHitl.mode !== 'never_ask') {
|
|
204
|
+
const argsHash = hashToolArgs(call.arguments);
|
|
205
|
+
const waitTokenId = computeToolCallWaitToken({
|
|
206
|
+
runId: kctx.runId as unknown as string,
|
|
207
|
+
callId: call.id,
|
|
208
|
+
argsHash,
|
|
209
|
+
});
|
|
210
|
+
const timeoutMs = effectiveHitl.timeoutMs;
|
|
211
|
+
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
212
|
+
|
|
213
|
+
if (ctx.bindings.hitl?.enqueue !== undefined) {
|
|
214
|
+
try {
|
|
215
|
+
await ctx.bindings.hitl.enqueue({
|
|
216
|
+
tenantId: ctx.input.tenantId,
|
|
217
|
+
subjectKind: 'tool-call:pending',
|
|
218
|
+
subjectRef: {
|
|
219
|
+
conversationId: ctx.input.conversationId,
|
|
220
|
+
agentId: ctx.input.agent.id,
|
|
221
|
+
agentVersion: ctx.input.agent.version,
|
|
222
|
+
toolId: tool.id,
|
|
223
|
+
toolVersion: resolvedVersion,
|
|
224
|
+
callId: call.id,
|
|
225
|
+
argsHash,
|
|
226
|
+
arguments: call.arguments as never,
|
|
227
|
+
},
|
|
228
|
+
requiredRole: resolvedHitl.requiredRole,
|
|
229
|
+
title: `HITL review: ${call.name}`,
|
|
230
|
+
description: `Tool call ${call.name} awaiting reviewer approval before dispatch.`,
|
|
231
|
+
waitTokenId,
|
|
232
|
+
provenanceRef: { runId: kctx.runId },
|
|
233
|
+
expiresAt: expiresAt as never,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
// Enqueue failure is soft — waitForToken parks either way.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const decision = await kctx.waitForToken<ToolHitlDecision>(waitTokenId, {
|
|
242
|
+
timeoutMs,
|
|
243
|
+
});
|
|
244
|
+
if (decision.decided === 'reject') {
|
|
245
|
+
toolRejectionPayload =
|
|
246
|
+
decision.rationale !== undefined ? { rationale: decision.rationale } : {};
|
|
247
|
+
}
|
|
248
|
+
// approve → fall through to dispatch
|
|
249
|
+
} catch (cause) {
|
|
250
|
+
if (cause instanceof WaitpointCancelledError) {
|
|
251
|
+
throwAgentTurnFailure({
|
|
252
|
+
code: 'hitl-cancelled',
|
|
253
|
+
message: `Tool-call HITL cancelled for ${call.name}: ${cause.reason}`,
|
|
254
|
+
reason: cause.reason,
|
|
255
|
+
} as never);
|
|
256
|
+
}
|
|
257
|
+
throw cause;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const toolStarted = Date.now();
|
|
262
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
263
|
+
kind: 'tool.started',
|
|
264
|
+
step: partial.step,
|
|
265
|
+
toolId: call.name,
|
|
266
|
+
toolVersion: resolvedVersion,
|
|
267
|
+
toolVersionRange: requestedRange,
|
|
268
|
+
invocationId: call.id,
|
|
269
|
+
arguments: call.arguments,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// If the reviewer rejected, synthesize a tool-result message
|
|
273
|
+
// carrying the rationale — the LLM reads it in the next
|
|
274
|
+
// iteration and can adapt (retry with different args, ask the
|
|
275
|
+
// user, apologize). NOT a run failure — reject is feedback, not
|
|
276
|
+
// termination.
|
|
277
|
+
if (toolRejectionPayload !== null) {
|
|
278
|
+
const rejectResult = {
|
|
279
|
+
status: 'rejected' as const,
|
|
280
|
+
...(toolRejectionPayload.rationale !== undefined && {
|
|
281
|
+
rationale: toolRejectionPayload.rationale,
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
nextMessages = await appendToolResult(ctx, call, rejectResult, {
|
|
285
|
+
toolId: tool.id as unknown as string,
|
|
286
|
+
nextMessages,
|
|
287
|
+
iterationAppended,
|
|
288
|
+
});
|
|
289
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
290
|
+
kind: 'tool.completed',
|
|
291
|
+
step: partial.step,
|
|
292
|
+
toolId: call.name,
|
|
293
|
+
toolVersion: resolvedVersion,
|
|
294
|
+
toolVersionRange: requestedRange,
|
|
295
|
+
invocationId: call.id,
|
|
296
|
+
output: rejectResult as never,
|
|
297
|
+
durationMs: Date.now() - toolStarted,
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const dispatched = await dispatchOne(ctx, tool, call, kctx.runId as unknown as string);
|
|
303
|
+
if (dispatched.kind === 'err') {
|
|
304
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
305
|
+
kind: 'tool.failed',
|
|
306
|
+
step: partial.step,
|
|
307
|
+
toolId: call.name,
|
|
308
|
+
invocationId: call.id,
|
|
309
|
+
error: {
|
|
310
|
+
code: dispatched.error.code,
|
|
311
|
+
message: dispatched.error.message,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
nextMessages = await retryOrFail(
|
|
315
|
+
ctx,
|
|
316
|
+
call,
|
|
317
|
+
dispatched.error,
|
|
318
|
+
dispatched.error.validationIssues,
|
|
319
|
+
{ toolId: tool.id as unknown as string, nextMessages, iterationAppended },
|
|
320
|
+
);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
ctx.appended.push(dispatched.value.persisted);
|
|
325
|
+
iterationAppended.push(dispatched.value.persisted);
|
|
326
|
+
nextMessages = [...nextMessages, dispatched.value.toolMessage];
|
|
327
|
+
|
|
328
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
329
|
+
kind: 'tool.completed',
|
|
330
|
+
step: partial.step,
|
|
331
|
+
toolId: call.name,
|
|
332
|
+
toolVersion: resolvedVersion,
|
|
333
|
+
toolVersionRange: requestedRange,
|
|
334
|
+
invocationId: call.id,
|
|
335
|
+
output: dispatched.value.persisted.content,
|
|
336
|
+
durationMs: Date.now() - toolStarted,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (ctx.provenance !== undefined) {
|
|
340
|
+
const modelCallNodeId = `model-call:${partial.step}`;
|
|
341
|
+
const toolCallNodeId = `tool-call:${call.id}`;
|
|
342
|
+
const toolResultNodeId = `tool-result:${call.id}`;
|
|
343
|
+
ctx.provenance.addNode({
|
|
344
|
+
id: toolCallNodeId,
|
|
345
|
+
kind: 'tool-call',
|
|
346
|
+
timestamp: dispatched.value.persisted.createdAt,
|
|
347
|
+
attributes: {
|
|
348
|
+
toolId: call.name,
|
|
349
|
+
invocationId: call.id,
|
|
350
|
+
// Capture the exact version the
|
|
351
|
+
// registry picked at run start + the range the agent asked
|
|
352
|
+
// for, so a replay can pin against the same version.
|
|
353
|
+
toolVersion: resolvedVersion,
|
|
354
|
+
toolVersionRange: requestedRange,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
ctx.provenance.addNode({
|
|
358
|
+
id: toolResultNodeId,
|
|
359
|
+
kind: 'tool-result',
|
|
360
|
+
timestamp: dispatched.value.persisted.createdAt,
|
|
361
|
+
});
|
|
362
|
+
ctx.provenance.addEdge({
|
|
363
|
+
from: toolCallNodeId,
|
|
364
|
+
to: modelCallNodeId,
|
|
365
|
+
kind: 'invoked',
|
|
366
|
+
});
|
|
367
|
+
ctx.provenance.addEdge({
|
|
368
|
+
from: toolResultNodeId,
|
|
369
|
+
to: toolCallNodeId,
|
|
370
|
+
kind: 'produced',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const forwarded: Partial<AgentTurnIterationOutput> & {
|
|
376
|
+
readonly step: number;
|
|
377
|
+
readonly hasToolCalls: true;
|
|
378
|
+
} = {
|
|
379
|
+
step: partial.step,
|
|
380
|
+
finishReason: partial.finishReason,
|
|
381
|
+
message: assistantMsg,
|
|
382
|
+
iterationAppended,
|
|
383
|
+
iterationUsage: partial.iterationUsage,
|
|
384
|
+
provider: partial.provider,
|
|
385
|
+
nextMessages,
|
|
386
|
+
hasToolCalls: true,
|
|
387
|
+
};
|
|
388
|
+
return forwarded;
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Store the assistant message that makes the calls — or, resumed after a
|
|
394
|
+
* park in this step, take the one stored before the park.
|
|
395
|
+
*/
|
|
396
|
+
async function persistToolCallMessage(
|
|
397
|
+
ctx: TurnContext,
|
|
398
|
+
assistantMsg: ModelMessage,
|
|
399
|
+
step: number,
|
|
400
|
+
): Promise<ConversationMessage> {
|
|
401
|
+
const callIds = (assistantMsg.toolCalls ?? []).map((tc) => tc.id);
|
|
402
|
+
const stored = takeStoredBeforePark(ctx, (m) => m.role === 'agent' && sameCalls(m, callIds));
|
|
403
|
+
if (stored !== undefined) return stored;
|
|
404
|
+
const persisted = await ctx.bindings.conversationBinding.appendMessage({
|
|
405
|
+
tenantId: ctx.input.tenantId,
|
|
406
|
+
conversationId: ctx.input.conversationId,
|
|
407
|
+
role: 'agent',
|
|
408
|
+
content: {
|
|
409
|
+
text: assistantMsg.content,
|
|
410
|
+
toolCalls: (assistantMsg.toolCalls ?? []).map((tc) => ({ ...tc })),
|
|
411
|
+
},
|
|
412
|
+
actor: ctx.input.agent.id,
|
|
413
|
+
isIntermediate: true,
|
|
414
|
+
});
|
|
415
|
+
if (persisted.kind === 'err') throwAgentTurnFailure(persisted.error);
|
|
416
|
+
ctx.appended.push(persisted.value);
|
|
417
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
418
|
+
kind: 'agent.message',
|
|
419
|
+
step,
|
|
420
|
+
isFinal: false,
|
|
421
|
+
message: persisted.value,
|
|
422
|
+
});
|
|
423
|
+
return persisted.value;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Take the first message stored before the park that matches. */
|
|
427
|
+
function takeStoredBeforePark(
|
|
428
|
+
ctx: TurnContext,
|
|
429
|
+
matches: (m: ConversationMessage) => boolean,
|
|
430
|
+
): ConversationMessage | undefined {
|
|
431
|
+
const index = ctx.storedBeforePark?.findIndex(matches) ?? -1;
|
|
432
|
+
if (index < 0) return undefined;
|
|
433
|
+
return ctx.storedBeforePark?.splice(index, 1)[0];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function sameCalls(m: ConversationMessage, callIds: readonly string[]): boolean {
|
|
437
|
+
const calls = (m.content as { readonly toolCalls?: readonly { readonly id?: unknown }[] })
|
|
438
|
+
.toolCalls;
|
|
439
|
+
return (
|
|
440
|
+
Array.isArray(calls) &&
|
|
441
|
+
calls.length === callIds.length &&
|
|
442
|
+
calls.every((c, i) => c.id === callIds[i])
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** A stored tool result, as the model sees it. */
|
|
447
|
+
function toolMessageOf(stored: ConversationMessage, callId: string): ModelMessage {
|
|
448
|
+
return {
|
|
449
|
+
role: 'tool',
|
|
450
|
+
content: typeof stored.content === 'string' ? stored.content : JSON.stringify(stored.content),
|
|
451
|
+
toolCallId: callId,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
interface ToolResultTarget {
|
|
456
|
+
/** The tool the result is for: its id, or the name the model used when the agent has no such tool. */
|
|
457
|
+
readonly toolId: string;
|
|
458
|
+
readonly nextMessages: readonly ModelMessage[];
|
|
459
|
+
readonly iterationAppended: ConversationMessage[];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Record a result the framework writes for a call (a rejection, a failed
|
|
464
|
+
* call sent back to the model) the way a tool's own result is recorded:
|
|
465
|
+
* persisted to the conversation with the call it answers, and added to
|
|
466
|
+
* the messages the next model call sees.
|
|
467
|
+
*/
|
|
468
|
+
async function appendToolResult(
|
|
469
|
+
ctx: TurnContext,
|
|
470
|
+
call: ModelToolCall,
|
|
471
|
+
output: unknown,
|
|
472
|
+
target: ToolResultTarget,
|
|
473
|
+
): Promise<ModelMessage[]> {
|
|
474
|
+
const persisted = await ctx.bindings.conversationBinding.appendMessage({
|
|
475
|
+
tenantId: ctx.input.tenantId,
|
|
476
|
+
conversationId: ctx.input.conversationId,
|
|
477
|
+
role: 'tool',
|
|
478
|
+
content: output as Record<string, unknown>,
|
|
479
|
+
toolCall: { toolId: target.toolId, invocationId: call.id },
|
|
480
|
+
});
|
|
481
|
+
if (persisted.kind === 'err') throwAgentTurnFailure(persisted.error);
|
|
482
|
+
ctx.appended.push(persisted.value);
|
|
483
|
+
target.iterationAppended.push(persisted.value);
|
|
484
|
+
return [
|
|
485
|
+
...target.nextMessages,
|
|
486
|
+
{ role: 'tool', content: JSON.stringify(output), toolCallId: call.id },
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A failed call the turn's policy retries goes back to the model as its
|
|
492
|
+
* result, while retries are left; anything else fails the turn, with the
|
|
493
|
+
* retries it took when the kind is one the policy retries.
|
|
494
|
+
*/
|
|
495
|
+
async function retryOrFail(
|
|
496
|
+
ctx: TurnContext,
|
|
497
|
+
call: ModelToolCall,
|
|
498
|
+
error: UnresolvedToolError | ToolInvocationError,
|
|
499
|
+
issues: readonly unknown[] | undefined,
|
|
500
|
+
target: ToolResultTarget,
|
|
501
|
+
): Promise<ModelMessage[]> {
|
|
502
|
+
const policy =
|
|
503
|
+
ctx.toolErrorPolicy ?? effectiveToolErrorPolicy(ctx.input.agent.toolErrors, undefined);
|
|
504
|
+
const kind = toolErrorKindOf(error);
|
|
505
|
+
const used = toolRetriesSoFar(target.nextMessages);
|
|
506
|
+
if (!policy.retryOn.has(kind)) throwAgentTurnFailure(error);
|
|
507
|
+
if (used >= policy.maxRetries) throwAgentTurnFailure({ ...error, toolRetries: used });
|
|
508
|
+
return appendToolResult(ctx, call, toolErrorResult(kind, error.message, issues), target);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function dispatchOne(
|
|
512
|
+
ctx: TurnContext,
|
|
513
|
+
tool: Tool,
|
|
514
|
+
call: ModelToolCall,
|
|
515
|
+
runId: string,
|
|
516
|
+
): Promise<
|
|
517
|
+
| {
|
|
518
|
+
readonly kind: 'ok';
|
|
519
|
+
readonly value: {
|
|
520
|
+
readonly persisted: ConversationMessage;
|
|
521
|
+
readonly toolMessage: ModelMessage;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
| {
|
|
525
|
+
readonly kind: 'err';
|
|
526
|
+
readonly error: {
|
|
527
|
+
readonly code: 'tool-invocation-failed';
|
|
528
|
+
readonly message: string;
|
|
529
|
+
readonly toolId: string;
|
|
530
|
+
readonly cause: unknown;
|
|
531
|
+
readonly validationIssues?: readonly Readonly<Record<string, unknown>>[];
|
|
532
|
+
readonly receivedInput?: unknown;
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
> {
|
|
536
|
+
const toolCtx: ToolContext = {
|
|
537
|
+
tenantId: ctx.input.tenantId,
|
|
538
|
+
runId,
|
|
539
|
+
requestId: call.id,
|
|
540
|
+
abortSignal: ctx.turnAbort.signal,
|
|
541
|
+
// HTTP tools built via defineTool({spec: {kind: 'http'}}) resolve declared
|
|
542
|
+
// secret_refs at invoke time. Present iff the caller wired
|
|
543
|
+
// `bindings.resolveSecret` from a tenant-scoped `SecretBinding`.
|
|
544
|
+
...(ctx.bindings.resolveSecret !== undefined && { resolveSecret: ctx.bindings.resolveSecret }),
|
|
545
|
+
};
|
|
546
|
+
const result = await invokeTool(tool, call.arguments, toolCtx);
|
|
547
|
+
if (result.kind === 'err') {
|
|
548
|
+
// Hoist AJV validation detail from the inner error so it survives
|
|
549
|
+
// `toWireError` (which filters `cause` to avoid Error-instance /
|
|
550
|
+
// cycle hazards). Two validation paths emit different field names
|
|
551
|
+
// for the AJV array:
|
|
552
|
+
// - `packages/handler-runtime/src/handler-runner.ts` (pack code,
|
|
553
|
+
// run by the pack service) → `issues`
|
|
554
|
+
// - `packages/tools/src/invoke.ts` (in-process dispatch) → `errors`
|
|
555
|
+
// Accept either. If neither shape is present, no hoisting —
|
|
556
|
+
// the outer `tool-invocation-failed` stays minimal.
|
|
557
|
+
const inner = result.error as {
|
|
558
|
+
readonly code?: string;
|
|
559
|
+
readonly issues?: readonly Readonly<Record<string, unknown>>[];
|
|
560
|
+
readonly errors?: readonly Readonly<Record<string, unknown>>[];
|
|
561
|
+
};
|
|
562
|
+
const rawIssues = Array.isArray(inner.issues)
|
|
563
|
+
? inner.issues
|
|
564
|
+
: Array.isArray(inner.errors)
|
|
565
|
+
? inner.errors
|
|
566
|
+
: undefined;
|
|
567
|
+
const isValidationFailure = inner.code === 'input-validation-failed' && rawIssues !== undefined;
|
|
568
|
+
return {
|
|
569
|
+
kind: 'err',
|
|
570
|
+
error: {
|
|
571
|
+
code: 'tool-invocation-failed',
|
|
572
|
+
message: result.error.message,
|
|
573
|
+
toolId: call.name,
|
|
574
|
+
cause: result.error,
|
|
575
|
+
...(isValidationFailure && {
|
|
576
|
+
validationIssues: rawIssues,
|
|
577
|
+
receivedInput: truncateForWire(call.arguments),
|
|
578
|
+
}),
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const persist = await ctx.bindings.conversationBinding.appendMessage({
|
|
583
|
+
tenantId: ctx.input.tenantId,
|
|
584
|
+
conversationId: ctx.input.conversationId,
|
|
585
|
+
role: 'tool',
|
|
586
|
+
content: (result.value as unknown as string | Record<string, unknown>) ?? '',
|
|
587
|
+
toolCall: { toolId: tool.id, invocationId: call.id },
|
|
588
|
+
});
|
|
589
|
+
if (persist.kind === 'err') {
|
|
590
|
+
// A DB failure persisting the tool result surfaces as the more
|
|
591
|
+
// generic invocation-failed to keep the caller's error shape stable.
|
|
592
|
+
return {
|
|
593
|
+
kind: 'err',
|
|
594
|
+
error: {
|
|
595
|
+
code: 'tool-invocation-failed',
|
|
596
|
+
message: persist.error.message,
|
|
597
|
+
toolId: tool.id,
|
|
598
|
+
cause: persist.error,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
kind: 'ok',
|
|
604
|
+
value: {
|
|
605
|
+
persisted: persist.value,
|
|
606
|
+
toolMessage: {
|
|
607
|
+
role: 'tool',
|
|
608
|
+
content: typeof result.value === 'string' ? result.value : JSON.stringify(result.value),
|
|
609
|
+
toolCallId: call.id,
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Cap `receivedInput` on validation errors so a huge tool-arg blob
|
|
617
|
+
* doesn't bloat the wire response body. 4 KiB stringified is
|
|
618
|
+
* comfortably larger than a typical LLM tool-call payload
|
|
619
|
+
* (place-name, query, message) but small enough that a
|
|
620
|
+
* pathological array/document truncates rather than round-trips
|
|
621
|
+
* megabytes. Truncated payloads land as a string with a trailing
|
|
622
|
+
* marker so callers can tell the value was elided.
|
|
623
|
+
*/
|
|
624
|
+
function truncateForWire(input: unknown, maxBytes = 4096): unknown {
|
|
625
|
+
let serialized: string;
|
|
626
|
+
try {
|
|
627
|
+
serialized = JSON.stringify(input);
|
|
628
|
+
} catch {
|
|
629
|
+
return '<unserializable input>';
|
|
630
|
+
}
|
|
631
|
+
if (Buffer.byteLength(serialized, 'utf8') <= maxBytes) return input;
|
|
632
|
+
return `${serialized.slice(0, maxBytes)}… (truncated at ${maxBytes} bytes)`;
|
|
633
|
+
}
|