@kindgi/handler-runtime 0.1.4-rc.5 → 0.1.5-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/handler-runner.d.ts +21 -3
- package/dist/handler-runner.d.ts.map +1 -1
- package/dist/handler-runner.js +40 -0
- package/dist/handler-runner.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kindgi-index.d.ts +50 -10
- package/dist/kindgi-index.d.ts.map +1 -1
- package/dist/kindgi-index.js +154 -12
- package/dist/kindgi-index.js.map +1 -1
- package/dist/pack-env.d.ts +3 -3
- package/dist/pack-service/main.d.ts +8 -2
- package/dist/pack-service/main.d.ts.map +1 -1
- package/dist/pack-service/main.js +38 -19
- package/dist/pack-service/main.js.map +1 -1
- package/dist/pack-service/records.d.ts +45 -0
- package/dist/pack-service/records.d.ts.map +1 -0
- package/dist/pack-service/records.js +73 -0
- package/dist/pack-service/records.js.map +1 -0
- package/dist/pack-service/service.d.ts +7 -0
- package/dist/pack-service/service.d.ts.map +1 -1
- package/dist/pack-service/service.js +75 -13
- package/dist/pack-service/service.js.map +1 -1
- package/dist/pack-service/supervisor.d.ts +24 -1
- package/dist/pack-service/supervisor.d.ts.map +1 -1
- package/dist/pack-service/supervisor.js +53 -10
- package/dist/pack-service/supervisor.js.map +1 -1
- package/dist/protocol.d.ts +7 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/protocol.js.map +1 -1
- package/package.json +9 -7
- package/src/handler-runner.ts +62 -4
- package/src/index.ts +6 -0
- package/src/kindgi-index.ts +192 -17
- package/src/pack-env.ts +3 -3
- package/src/pack-service/main.ts +39 -20
- package/src/pack-service/records.ts +99 -0
- package/src/pack-service/service.ts +96 -16
- package/src/pack-service/supervisor.ts +69 -12
- package/src/protocol.ts +7 -2
package/src/handler-runner.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* 3. calls `handler(input, ctx)`;
|
|
17
17
|
* 4. validates the output against the tool's output JSON Schema.
|
|
18
18
|
*
|
|
19
|
-
* A check
|
|
19
|
+
* A check validates the config against the guardrail's `configSchema`
|
|
20
|
+
* (as sent), imports the module, resolves its `evaluate`, and calls
|
|
20
21
|
* `evaluate(config, trace, bindings)`.
|
|
21
22
|
*
|
|
22
23
|
* Neither throws: every failure is a typed `HandlerError`. The runner
|
|
@@ -24,17 +25,19 @@
|
|
|
24
25
|
* pack code is the team's own (trusted).
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
|
-
import type { ValidateFunction } from 'ajv';
|
|
28
|
+
import type { ErrorObject, ValidateFunction } from 'ajv';
|
|
28
29
|
import * as addFormatsModule from 'ajv-formats';
|
|
29
30
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
30
31
|
|
|
32
|
+
import type { Logger } from '@kindgi/log';
|
|
31
33
|
import { type ZodLikeSchema, isZodSchema, parseWithSchema } from '@kindgi/schema';
|
|
32
34
|
import type { Result } from '@kindgi/types';
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* What a handler gets beside its input: the tenant and run it serves,
|
|
36
|
-
* and
|
|
37
|
-
* the
|
|
38
|
+
* and the "typed needs" it declares (`needsSpec.env`, `needsSpec.secrets`),
|
|
39
|
+
* resolved by the runtime for this call and sent with it. `config` is
|
|
40
|
+
* reserved: no runtime sends it yet.
|
|
38
41
|
*/
|
|
39
42
|
export interface HandlerContext {
|
|
40
43
|
readonly tenantId: string;
|
|
@@ -44,6 +47,7 @@ export interface HandlerContext {
|
|
|
44
47
|
/** The project's org, when it has one (2.3.0). */
|
|
45
48
|
readonly orgId?: string;
|
|
46
49
|
readonly requestId?: string;
|
|
50
|
+
/** The declared env values: project, else org, else tenant (protocol 2.5.0). */
|
|
47
51
|
readonly env?: Readonly<Record<string, unknown>>;
|
|
48
52
|
readonly secrets?: Readonly<Record<string, unknown>>;
|
|
49
53
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
@@ -59,6 +63,12 @@ export interface HandlerContext {
|
|
|
59
63
|
* should pass it on so they stop promptly.
|
|
60
64
|
*/
|
|
61
65
|
readonly abortSignal?: AbortSignal;
|
|
66
|
+
/**
|
|
67
|
+
* Never on the wire; the pack service adds it: a logger bound to the
|
|
68
|
+
* call (its tenant, run, request and trace). Not enumerable, like
|
|
69
|
+
* `secrets`.
|
|
70
|
+
*/
|
|
71
|
+
readonly log?: Logger;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/**
|
|
@@ -87,6 +97,14 @@ export interface ToolInvocationSpec {
|
|
|
87
97
|
export interface CheckInvocationSpec {
|
|
88
98
|
readonly id: string;
|
|
89
99
|
readonly modulePath: string;
|
|
100
|
+
/**
|
|
101
|
+
* The guardrail's `configSchema` from the index. A call's `config` that
|
|
102
|
+
* doesn't fit it is refused (`input-validation-failed`, with the
|
|
103
|
+
* issues) before the check runs. It is checked as sent, the schema's
|
|
104
|
+
* defaults not filled in, as the indexer checks a declared config; the
|
|
105
|
+
* check's own schema fills them in.
|
|
106
|
+
*/
|
|
107
|
+
readonly configSchema?: Readonly<Record<string, unknown>>;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
/** Every way a call can fail; the pack service answers with the same code. */
|
|
@@ -367,6 +385,35 @@ export async function runCheck(options: RunCheckOptions): Promise<Result<unknown
|
|
|
367
385
|
const { check, config, trace } = options;
|
|
368
386
|
const importCheck = options.importCheck ?? defaultImportCheck;
|
|
369
387
|
|
|
388
|
+
if (check.configSchema !== undefined) {
|
|
389
|
+
let configValidator: ValidateFunction;
|
|
390
|
+
try {
|
|
391
|
+
configValidator = compileSchema(check.configSchema, 'output');
|
|
392
|
+
} catch (cause) {
|
|
393
|
+
return {
|
|
394
|
+
kind: 'err',
|
|
395
|
+
error: {
|
|
396
|
+
code: 'input-validation-failed',
|
|
397
|
+
message: `Check "${check.id}" config schema failed to compile: ${stringifyError(cause)}`,
|
|
398
|
+
toolId: check.id,
|
|
399
|
+
cause: serializeCause(cause),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (!configValidator(config)) {
|
|
404
|
+
const issues = configValidator.errors ?? [];
|
|
405
|
+
return {
|
|
406
|
+
kind: 'err',
|
|
407
|
+
error: {
|
|
408
|
+
code: 'input-validation-failed',
|
|
409
|
+
message: `Check "${check.id}" config failed validation${describeFirstIssue(issues)}`,
|
|
410
|
+
toolId: check.id,
|
|
411
|
+
issues,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
370
417
|
let module_: CheckModule;
|
|
371
418
|
try {
|
|
372
419
|
module_ = await importCheck(check.modulePath);
|
|
@@ -414,6 +461,17 @@ export async function runCheck(options: RunCheckOptions): Promise<Result<unknown
|
|
|
414
461
|
return { kind: 'ok', value: result };
|
|
415
462
|
}
|
|
416
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Where the first issue is and what it says (` at /maxChars: must be >= 0`),
|
|
466
|
+
* for a message that is read without its issues: a runtime reports a
|
|
467
|
+
* check's error by its code and message alone.
|
|
468
|
+
*/
|
|
469
|
+
function describeFirstIssue(issues: readonly ErrorObject[]): string {
|
|
470
|
+
const first = issues[0];
|
|
471
|
+
if (first === undefined) return '';
|
|
472
|
+
return `${first.instancePath === '' ? '' : ` at ${first.instancePath}`}: ${first.message ?? 'invalid'}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
417
475
|
function resolveCheckEvaluate(
|
|
418
476
|
module_: CheckModule,
|
|
419
477
|
):
|
package/src/index.ts
CHANGED
|
@@ -32,17 +32,23 @@ export type {
|
|
|
32
32
|
KindgiProviderDeclaration,
|
|
33
33
|
KindgiConfigFile,
|
|
34
34
|
LoadKindgiConfigOptions,
|
|
35
|
+
JvmLanguage,
|
|
35
36
|
PackLanguage,
|
|
36
37
|
} from './kindgi-index.js';
|
|
37
38
|
export {
|
|
38
39
|
DEFAULT_DISCOVERY,
|
|
40
|
+
DEFAULT_JAVA_DISCOVERY,
|
|
41
|
+
DEFAULT_SCALA_DISCOVERY,
|
|
39
42
|
DEFAULT_PYTHON_DISCOVERY,
|
|
40
43
|
HELP_TEXT as KINDGI_INDEX_HELP_TEXT,
|
|
41
44
|
INDEX_ENVELOPE_VERSION,
|
|
42
45
|
KERNEL_PAYLOAD_VERSION,
|
|
43
46
|
KINDGI_CONFIG_FILENAMES,
|
|
47
|
+
KINDGI_JSON_CONFIG_FILENAME,
|
|
44
48
|
PYPROJECT_FILENAME,
|
|
49
|
+
RESERVED_CHECK_IDS,
|
|
45
50
|
findKindgiConfig,
|
|
51
|
+
isJvmLanguage,
|
|
46
52
|
loadKindgiConfig,
|
|
47
53
|
packLanguage,
|
|
48
54
|
resolveDiscovery,
|
package/src/kindgi-index.ts
CHANGED
|
@@ -89,9 +89,17 @@ export interface DiscoveryConfig {
|
|
|
89
89
|
/**
|
|
90
90
|
* The language a pack's code (tool handlers, guardrail checks) is written
|
|
91
91
|
* in — which indexer reads it and which pack service runs it. Agents and
|
|
92
|
-
* flows are data in
|
|
92
|
+
* flows are data in any of them.
|
|
93
93
|
*/
|
|
94
|
-
export type PackLanguage = 'node' | 'python';
|
|
94
|
+
export type PackLanguage = 'node' | 'python' | 'java' | 'scala';
|
|
95
|
+
|
|
96
|
+
/** The languages that run on the JVM: built by Maven (Java) or sbt (Scala), indexed and served by kindgi-pack. */
|
|
97
|
+
export type JvmLanguage = Extract<PackLanguage, 'java' | 'scala'>;
|
|
98
|
+
|
|
99
|
+
/** Whether a pack's code runs on the JVM (kindgi-pack's indexer and pack service). */
|
|
100
|
+
export function isJvmLanguage(language: PackLanguage): language is JvmLanguage {
|
|
101
|
+
return language === 'java' || language === 'scala';
|
|
102
|
+
}
|
|
95
103
|
|
|
96
104
|
/**
|
|
97
105
|
* The subset of `kindgi.config.ts` this indexer consumes. Other
|
|
@@ -108,7 +116,7 @@ export interface KindgiConfig {
|
|
|
108
116
|
/**
|
|
109
117
|
* The pack's code language. Absent in `kindgi.config.*`: `node`. A
|
|
110
118
|
* `[tool.kindgi]` table in `pyproject.toml` is a Python pack unless it
|
|
111
|
-
* says otherwise.
|
|
119
|
+
* says otherwise. A `kindgi.config.json` says `"java"` or `"scala"`.
|
|
112
120
|
*/
|
|
113
121
|
readonly language?: PackLanguage;
|
|
114
122
|
/**
|
|
@@ -185,15 +193,53 @@ export const DEFAULT_PYTHON_DISCOVERY: Required<DiscoveryConfig> = {
|
|
|
185
193
|
flows: 'flows/**/*.py',
|
|
186
194
|
};
|
|
187
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Default discovery patterns of a Java pack (`com.kindgi.pack.Main index`
|
|
198
|
+
* reads the same keys from `kindgi.config.json`): the source files under
|
|
199
|
+
* any `tools`, `guardrails`, `agents` or `flows` package.
|
|
200
|
+
*/
|
|
201
|
+
export const DEFAULT_JAVA_DISCOVERY: Required<DiscoveryConfig> = {
|
|
202
|
+
tools: 'src/main/java/**/tools/**/*.java',
|
|
203
|
+
guardrails: 'src/main/java/**/guardrails/**/*.java',
|
|
204
|
+
agents: 'src/main/java/**/agents/**/*.java',
|
|
205
|
+
flows: 'src/main/java/**/flows/**/*.java',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Default discovery patterns of a Scala pack (kindgi-pack's indexer reads
|
|
210
|
+
* the same keys): the source files under any `tools`, `guardrails`, `agents`
|
|
211
|
+
* or `flows` package.
|
|
212
|
+
*/
|
|
213
|
+
export const DEFAULT_SCALA_DISCOVERY: Required<DiscoveryConfig> = {
|
|
214
|
+
tools: 'src/main/scala/**/tools/**/*.scala',
|
|
215
|
+
guardrails: 'src/main/scala/**/guardrails/**/*.scala',
|
|
216
|
+
agents: 'src/main/scala/**/agents/**/*.scala',
|
|
217
|
+
flows: 'src/main/scala/**/flows/**/*.scala',
|
|
218
|
+
};
|
|
219
|
+
|
|
188
220
|
/** A config's discovery patterns with the language's defaults filled in. */
|
|
189
221
|
export function resolveDiscovery(
|
|
190
222
|
discovery: DiscoveryConfig | undefined,
|
|
191
223
|
language: PackLanguage = 'node',
|
|
192
224
|
): Required<DiscoveryConfig> {
|
|
193
|
-
const defaults =
|
|
225
|
+
const defaults =
|
|
226
|
+
language === 'python'
|
|
227
|
+
? DEFAULT_PYTHON_DISCOVERY
|
|
228
|
+
: language === 'java'
|
|
229
|
+
? DEFAULT_JAVA_DISCOVERY
|
|
230
|
+
: language === 'scala'
|
|
231
|
+
? DEFAULT_SCALA_DISCOVERY
|
|
232
|
+
: DEFAULT_DISCOVERY;
|
|
194
233
|
return { ...defaults, ...(discovery ?? {}) };
|
|
195
234
|
}
|
|
196
235
|
|
|
236
|
+
/** The command that indexes a pack of each language other than Node. */
|
|
237
|
+
const OWN_INDEXER: Readonly<Record<Exclude<PackLanguage, 'node'>, string>> = {
|
|
238
|
+
python: 'python -m kindgi.pack index',
|
|
239
|
+
java: 'java -cp <classpath> com.kindgi.pack.Main index',
|
|
240
|
+
scala: 'java -cp <classpath> com.kindgi.pack.Main index',
|
|
241
|
+
};
|
|
242
|
+
|
|
197
243
|
// -----------------------------------------------------------------------
|
|
198
244
|
// Manifest entry shapes emitted in index.json
|
|
199
245
|
// -----------------------------------------------------------------------
|
|
@@ -272,6 +318,8 @@ export interface IndexedAgent {
|
|
|
272
318
|
readonly output?: Readonly<Record<string, unknown>>;
|
|
273
319
|
/** How the agent's turns retry failed tool calls (`{ maxRetries?, retryOn? }`). */
|
|
274
320
|
readonly toolErrors?: Readonly<Record<string, unknown>>;
|
|
321
|
+
/** How the agent uses what it retrieves (`{ instructionTypes? }`). */
|
|
322
|
+
readonly memory?: Readonly<Record<string, unknown>>;
|
|
275
323
|
readonly modulePath: string;
|
|
276
324
|
}
|
|
277
325
|
|
|
@@ -329,7 +377,8 @@ export type IndexerErrorCode =
|
|
|
329
377
|
| 'zod-conversion-failed'
|
|
330
378
|
| 'manifest-validation-failed'
|
|
331
379
|
| 'output-write-failed'
|
|
332
|
-
| 'language-mismatch'
|
|
380
|
+
| 'language-mismatch'
|
|
381
|
+
| 'reserved-check-id';
|
|
333
382
|
|
|
334
383
|
export interface IndexerError {
|
|
335
384
|
readonly code: IndexerErrorCode;
|
|
@@ -434,7 +483,7 @@ export async function runIndexer(
|
|
|
434
483
|
kind: 'err',
|
|
435
484
|
error: {
|
|
436
485
|
code: 'language-mismatch',
|
|
437
|
-
message: `${packDir} is a ${packLanguage(config)} pack; index it with its own indexer (
|
|
486
|
+
message: `${packDir} is a ${packLanguage(config)} pack; index it with its own indexer (${OWN_INDEXER[packLanguage(config) as Exclude<PackLanguage, 'node'>]})`,
|
|
438
487
|
},
|
|
439
488
|
};
|
|
440
489
|
}
|
|
@@ -590,6 +639,11 @@ export async function runIndexer(
|
|
|
590
639
|
fileErrors.push(built.error);
|
|
591
640
|
continue;
|
|
592
641
|
}
|
|
642
|
+
const reserved = reservedCheckIn(unwrapped, module_, relPath);
|
|
643
|
+
if (reserved !== undefined) {
|
|
644
|
+
fileErrors.push(reserved);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
593
647
|
const duplicate = duplicateOf('guardrail', built.value.id, undefined, relPath);
|
|
594
648
|
if (duplicate !== undefined) {
|
|
595
649
|
fileErrors.push(duplicate);
|
|
@@ -937,19 +991,49 @@ export async function loadKindgiConfig(
|
|
|
937
991
|
/** `pyproject.toml` — a Python pack keeps its config in the `[tool.kindgi]` table. */
|
|
938
992
|
export const PYPROJECT_FILENAME = 'pyproject.toml';
|
|
939
993
|
|
|
994
|
+
/**
|
|
995
|
+
* `kindgi.config.json` — a JVM pack's config (`"language": "java"`): the
|
|
996
|
+
* same keys as `kindgi.config.ts`, as JSON, so Maven, Gradle and sbt builds
|
|
997
|
+
* share one file.
|
|
998
|
+
*/
|
|
999
|
+
export const KINDGI_JSON_CONFIG_FILENAME = 'kindgi.config.json';
|
|
1000
|
+
|
|
940
1001
|
export interface KindgiConfigFile {
|
|
941
1002
|
readonly path: string;
|
|
942
|
-
/**
|
|
943
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* `module`: a `kindgi.config.*` module; `pyproject`: the `[tool.kindgi]`
|
|
1005
|
+
* table of a `pyproject.toml`; `json`: a `kindgi.config.json`.
|
|
1006
|
+
*/
|
|
1007
|
+
readonly format: 'module' | 'pyproject' | 'json';
|
|
1008
|
+
/**
|
|
1009
|
+
* Another config file at the same root, next to a `kindgi.config.json`:
|
|
1010
|
+
* loading the config refuses the pack, naming both.
|
|
1011
|
+
*/
|
|
1012
|
+
readonly conflictsWith?: string;
|
|
944
1013
|
}
|
|
945
1014
|
|
|
946
1015
|
/**
|
|
947
|
-
* Where a pack's config lives: the first `kindgi.config.*` at
|
|
948
|
-
* (`KINDGI_CONFIG_FILENAMES` order), else a `pyproject.toml`
|
|
949
|
-
* `[tool.kindgi]` table
|
|
950
|
-
*
|
|
1016
|
+
* Where a pack's config lives: the first `kindgi.config.*` module at
|
|
1017
|
+
* `packDir` (`KINDGI_CONFIG_FILENAMES` order), else a `pyproject.toml`
|
|
1018
|
+
* with a `[tool.kindgi]` table; a `kindgi.config.json` when it is the only
|
|
1019
|
+
* one. `undefined` when none — a `pyproject.toml` without the table is a
|
|
1020
|
+
* Python project, not a pack. A `kindgi.config.json` next to another
|
|
1021
|
+
* config file is returned with `conflictsWith`, and the loader refuses it.
|
|
951
1022
|
*/
|
|
952
1023
|
export async function findKindgiConfig(packDir: string): Promise<KindgiConfigFile | undefined> {
|
|
1024
|
+
const other = await findModuleOrPyproject(packDir);
|
|
1025
|
+
const json = path.join(packDir, KINDGI_JSON_CONFIG_FILENAME);
|
|
1026
|
+
if (await exists(json)) {
|
|
1027
|
+
return {
|
|
1028
|
+
path: json,
|
|
1029
|
+
format: 'json',
|
|
1030
|
+
...(other !== undefined && { conflictsWith: other.path }),
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return other;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function findModuleOrPyproject(packDir: string): Promise<KindgiConfigFile | undefined> {
|
|
953
1037
|
for (const name of KINDGI_CONFIG_FILENAMES) {
|
|
954
1038
|
const candidate = path.join(packDir, name);
|
|
955
1039
|
if (await exists(candidate)) return { path: candidate, format: 'module' };
|
|
@@ -992,7 +1076,9 @@ async function loadConfig(
|
|
|
992
1076
|
if (explicitPath !== undefined) {
|
|
993
1077
|
const resolved = path.resolve(explicitPath);
|
|
994
1078
|
if (await exists(resolved)) {
|
|
995
|
-
const
|
|
1079
|
+
const base = path.basename(resolved);
|
|
1080
|
+
const format =
|
|
1081
|
+
base === PYPROJECT_FILENAME ? 'pyproject' : base.endsWith('.json') ? 'json' : 'module';
|
|
996
1082
|
file = { path: resolved, format };
|
|
997
1083
|
}
|
|
998
1084
|
} else {
|
|
@@ -1003,16 +1089,32 @@ async function loadConfig(
|
|
|
1003
1089
|
kind: 'err',
|
|
1004
1090
|
error: {
|
|
1005
1091
|
code: 'config-not-found',
|
|
1006
|
-
message: `No kindgi.config.{ts,mts,mjs,js,cjs},
|
|
1092
|
+
message: `No kindgi.config.{ts,mts,mjs,js,cjs}, pyproject.toml with a [tool.kindgi] table, or kindgi.config.json, found at pack root ${packDir}`,
|
|
1093
|
+
},
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
if (file.conflictsWith !== undefined) {
|
|
1097
|
+
const other = path.basename(file.conflictsWith);
|
|
1098
|
+
return {
|
|
1099
|
+
kind: 'err',
|
|
1100
|
+
error: {
|
|
1101
|
+
code: 'config-invalid',
|
|
1102
|
+
message:
|
|
1103
|
+
`${packDir} has two pack configs, ${KINDGI_JSON_CONFIG_FILENAME} and ${other}; a pack has one. ` +
|
|
1104
|
+
`Keep ${KINDGI_JSON_CONFIG_FILENAME} for a Java or Scala pack, or ${other} for a ` +
|
|
1105
|
+
`${file.conflictsWith.endsWith(PYPROJECT_FILENAME) ? 'Python' : 'TypeScript'} one, and remove the other.`,
|
|
1106
|
+
filePath: file.path,
|
|
1007
1107
|
},
|
|
1008
1108
|
};
|
|
1009
1109
|
}
|
|
1010
1110
|
const read =
|
|
1011
1111
|
file.format === 'pyproject'
|
|
1012
1112
|
? await readPyprojectTable(file.path)
|
|
1013
|
-
:
|
|
1113
|
+
: file.format === 'json'
|
|
1114
|
+
? await readJsonConfig(file.path)
|
|
1115
|
+
: await importConfigModule(file.path, importModule);
|
|
1014
1116
|
if (read.kind === 'err') return read;
|
|
1015
|
-
const checked = checkConfig(read.value, file.path);
|
|
1117
|
+
const checked = checkConfig(read.value, file.path, file.format);
|
|
1016
1118
|
if (checked.kind === 'err') return checked;
|
|
1017
1119
|
const record = checked.value;
|
|
1018
1120
|
return {
|
|
@@ -1084,10 +1186,26 @@ async function readPyprojectTable(
|
|
|
1084
1186
|
return { kind: 'ok', value: table as Record<string, unknown> };
|
|
1085
1187
|
}
|
|
1086
1188
|
|
|
1189
|
+
async function readJsonConfig(
|
|
1190
|
+
filePath: string,
|
|
1191
|
+
): Promise<Result<Record<string, unknown>, IndexerError>> {
|
|
1192
|
+
let document: unknown;
|
|
1193
|
+
try {
|
|
1194
|
+
document = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
1195
|
+
} catch (cause) {
|
|
1196
|
+
return parseFailed(filePath, `Failed to read ${filePath}: ${stringifyError(cause)}`, cause);
|
|
1197
|
+
}
|
|
1198
|
+
if (!isObject(document)) {
|
|
1199
|
+
return parseFailed(filePath, `Config file ${filePath}: the file must hold a JSON object`);
|
|
1200
|
+
}
|
|
1201
|
+
return { kind: 'ok', value: document as Record<string, unknown> };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1087
1204
|
/** The checks every config passes, whatever file it came from. */
|
|
1088
1205
|
function checkConfig(
|
|
1089
1206
|
record: Record<string, unknown>,
|
|
1090
1207
|
filePath: string,
|
|
1208
|
+
format: KindgiConfigFile['format'],
|
|
1091
1209
|
): Result<KindgiConfig, IndexerError> {
|
|
1092
1210
|
const pack = record.pack;
|
|
1093
1211
|
if (typeof pack !== 'object' || pack === null) {
|
|
@@ -1109,7 +1227,19 @@ function checkConfig(
|
|
|
1109
1227
|
`Config file ${filePath}: 'pack.version' is missing or not a non-empty string`,
|
|
1110
1228
|
);
|
|
1111
1229
|
}
|
|
1112
|
-
if (
|
|
1230
|
+
if (format === 'json') {
|
|
1231
|
+
// kindgi.config.json is the JVM's config: it names its language.
|
|
1232
|
+
if (record.language !== 'java' && record.language !== 'scala') {
|
|
1233
|
+
return parseFailed(
|
|
1234
|
+
filePath,
|
|
1235
|
+
`Config file ${filePath}: ${KINDGI_JSON_CONFIG_FILENAME} is a JVM pack's config; it says "language": "java" or "scala"`,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
} else if (
|
|
1239
|
+
record.language !== undefined &&
|
|
1240
|
+
record.language !== 'node' &&
|
|
1241
|
+
record.language !== 'python'
|
|
1242
|
+
) {
|
|
1113
1243
|
return parseFailed(filePath, `Config file ${filePath}: 'language' must be "node" or "python"`);
|
|
1114
1244
|
}
|
|
1115
1245
|
return { kind: 'ok', value: record as KindgiConfig };
|
|
@@ -1368,6 +1498,50 @@ function buildTool(
|
|
|
1368
1498
|
return { kind: 'ok', value: tool };
|
|
1369
1499
|
}
|
|
1370
1500
|
|
|
1501
|
+
/**
|
|
1502
|
+
* The built-in checks' ids (`BUILT_IN_CHECK_IDS` in `@kindgi/guardrails`; a test holds the two
|
|
1503
|
+
* equal). A guardrail may name one (`check: 'must-cite'`) and the runtime runs the built-in; a
|
|
1504
|
+
* pack may never ship its own check under one, which the runtime would silently replace.
|
|
1505
|
+
*/
|
|
1506
|
+
export const RESERVED_CHECK_IDS: readonly string[] = [
|
|
1507
|
+
'must-cite',
|
|
1508
|
+
'never-call-tool',
|
|
1509
|
+
'max-tool-calls',
|
|
1510
|
+
'output-matches',
|
|
1511
|
+
'tool-order',
|
|
1512
|
+
'required-substring',
|
|
1513
|
+
'forbidden-substring',
|
|
1514
|
+
];
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* A check implementation (an object with an `id` and an `evaluate` function) under a built-in
|
|
1518
|
+
* id: the guardrail's own `check`, or any check the module exports. Naming a built-in by its id
|
|
1519
|
+
* (a string) is the way to use it, and is fine.
|
|
1520
|
+
*/
|
|
1521
|
+
function reservedCheckIn(
|
|
1522
|
+
guardrail: unknown,
|
|
1523
|
+
module_: unknown,
|
|
1524
|
+
relPath: string,
|
|
1525
|
+
): IndexerError | undefined {
|
|
1526
|
+
const candidates: unknown[] = [
|
|
1527
|
+
isObject(guardrail) ? (guardrail as Record<string, unknown>).check : undefined,
|
|
1528
|
+
...(isObject(module_) ? Object.values(module_ as Record<string, unknown>) : []),
|
|
1529
|
+
];
|
|
1530
|
+
for (const c of candidates) {
|
|
1531
|
+
if (!isObject(c)) continue;
|
|
1532
|
+
const rec = c as Record<string, unknown>;
|
|
1533
|
+
if (typeof rec.id !== 'string' || typeof rec.evaluate !== 'function') continue;
|
|
1534
|
+
if (!RESERVED_CHECK_IDS.includes(rec.id)) continue;
|
|
1535
|
+
return {
|
|
1536
|
+
code: 'reserved-check-id',
|
|
1537
|
+
message: `${relPath} ships its own check under "${rec.id}", a built-in check's id: a pack can't replace a built-in. Rename your check (for example "<pack>.checks.${rec.id}"), or, to use the built-in, name it (check: '${rec.id}') and drop your implementation.`,
|
|
1538
|
+
filePath: relPath,
|
|
1539
|
+
field: 'check',
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1371
1545
|
function buildGuardrail(
|
|
1372
1546
|
raw: unknown,
|
|
1373
1547
|
relPath: string,
|
|
@@ -1510,6 +1684,7 @@ function optionalAgentFields(rec: Record<string, unknown>): Partial<IndexedAgent
|
|
|
1510
1684
|
'output',
|
|
1511
1685
|
'toolErrors',
|
|
1512
1686
|
'modelSettings',
|
|
1687
|
+
'memory',
|
|
1513
1688
|
] as const) {
|
|
1514
1689
|
if (isObject(rec[key])) out[key] = rec[key];
|
|
1515
1690
|
}
|
package/src/pack-env.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* libraries they import take from `process.env` (a database URL a client
|
|
7
7
|
* reads at import, a bucket name). Declared once per pack, in
|
|
8
8
|
* `kindgi.config` (`env: { required, optional }`; Python:
|
|
9
|
-
* `[tool.kindgi.env]`), and carried in `index.json`, so the pack service
|
|
9
|
+
* `[tool.kindgi.env]`; Java and Scala: `env` in `kindgi.config.json`), and carried in `index.json`, so the pack service
|
|
10
10
|
* in an image knows what it needs.
|
|
11
11
|
*
|
|
12
12
|
* A deployment injects exactly these names: `required` must be there for
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* has a value. Nothing else from an env file reaches the process.
|
|
15
15
|
*
|
|
16
16
|
* Distinct from `needsSpec.env` / `ctx.env`, the per-call values the
|
|
17
|
-
* runtime resolves for a call's
|
|
18
|
-
* `ctx.secrets`.
|
|
17
|
+
* runtime resolves for a call (its project's, else its org's, else its
|
|
18
|
+
* tenant's, from `/v1/env`), and from `needsSpec.secrets` / `ctx.secrets`.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/** A pack's declared process environment, as `index.json` carries it. */
|
package/src/pack-service/main.ts
CHANGED
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
* broken build never becomes ready. SIGTERM drains in-flight calls
|
|
31
31
|
* (readyz answers 503 meanwhile) and exits 0.
|
|
32
32
|
*
|
|
33
|
-
* Logs are
|
|
34
|
-
*
|
|
33
|
+
* Logs are `@kindgi/log` records on stderr (subsystem `pack`), at
|
|
34
|
+
* `KINDGI_LOG_LEVEL` / `KINDGI_LOG_LEVELS`, in `KINDGI_LOG_FORMAT` (`auto`:
|
|
35
|
+
* JSON unless stderr is a terminal): a record per call, and the
|
|
36
|
+
* lifecycle (`listening`, `boot-failed`, `draining`, …) whatever the
|
|
37
|
+
* levels (`./records.ts`). The `listening` record carries the bound port,
|
|
38
|
+
* for callers that start it with `PORT=0`.
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
41
|
import { access, readFile } from 'node:fs/promises';
|
|
@@ -44,6 +48,7 @@ import { isProcessEntrypoint } from '../entrypoint.js';
|
|
|
44
48
|
import type { Index } from '../kindgi-index.js';
|
|
45
49
|
import { INDEX_ENVELOPE_VERSION, readBundleMap } from '../kindgi-index.js';
|
|
46
50
|
import { PACK_ENV_CHECK_VAR, type PackEnvCheck, parsePackEnvCheck } from '../pack-env.js';
|
|
51
|
+
import { type PackServiceLogs, defaultPackServiceLogs, packServiceLogs } from './records.js';
|
|
47
52
|
import { type PackService, type PackServiceLogEvent, createPackService } from './service.js';
|
|
48
53
|
|
|
49
54
|
export interface PackServiceConfig {
|
|
@@ -132,11 +137,20 @@ type StartOutcome =
|
|
|
132
137
|
| { readonly kind: 'ok'; readonly value: RunningPackService }
|
|
133
138
|
| { readonly kind: 'err'; readonly problems: readonly string[] };
|
|
134
139
|
|
|
135
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Load the index, check and prewarm every module, then listen. `logs`:
|
|
142
|
+
* where its records go (default: stderr, at the default levels). A
|
|
143
|
+
* function instead gets the service's events as before records, and no
|
|
144
|
+
* records are written.
|
|
145
|
+
*/
|
|
136
146
|
export async function startPackService(
|
|
137
147
|
config: PackServiceConfig,
|
|
138
|
-
|
|
148
|
+
logs:
|
|
149
|
+
| PackServiceLogs
|
|
150
|
+
| ((event: PackServiceLogEvent | Record<string, unknown>) => void) = defaultPackServiceLogs(),
|
|
139
151
|
): Promise<StartOutcome> {
|
|
152
|
+
const legacy = typeof logs === 'function' ? logs : undefined;
|
|
153
|
+
const records = typeof logs === 'function' ? undefined : logs;
|
|
140
154
|
let index: Index;
|
|
141
155
|
try {
|
|
142
156
|
index = JSON.parse(await readFile(config.indexPath, 'utf8')) as Index;
|
|
@@ -177,7 +191,10 @@ export async function startPackService(
|
|
|
177
191
|
token: config.token,
|
|
178
192
|
...(config.maxConcurrency !== undefined && { maxConcurrency: config.maxConcurrency }),
|
|
179
193
|
...(config.envCheck !== undefined && { envCheck: config.envCheck }),
|
|
180
|
-
logger,
|
|
194
|
+
...(legacy !== undefined && { logger: legacy }),
|
|
195
|
+
...(records !== undefined && {
|
|
196
|
+
log: records.log.child({ packId: index.packId, artifactVersion: index.artifactVersion }),
|
|
197
|
+
}),
|
|
181
198
|
});
|
|
182
199
|
const failures = await service.prewarm();
|
|
183
200
|
if (failures.length > 0) return { kind: 'err', problems: failures.map((f) => f.message) };
|
|
@@ -189,12 +206,9 @@ export async function startPackService(
|
|
|
189
206
|
: server.listen(config.port, config.host, ready),
|
|
190
207
|
);
|
|
191
208
|
const port = (server.address() as AddressInfo).port;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
packId: index.packId,
|
|
196
|
-
artifactVersion: index.artifactVersion,
|
|
197
|
-
});
|
|
209
|
+
const listening = { port, packId: index.packId, artifactVersion: index.artifactVersion };
|
|
210
|
+
legacy?.({ kind: 'listening', ...listening });
|
|
211
|
+
records?.event('listening', `Listening on port ${port}`, listening);
|
|
198
212
|
return {
|
|
199
213
|
kind: 'ok',
|
|
200
214
|
value: {
|
|
@@ -219,34 +233,39 @@ export async function main(
|
|
|
219
233
|
argv: readonly string[] = process.argv.slice(2),
|
|
220
234
|
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
221
235
|
): Promise<number> {
|
|
236
|
+
const built = packServiceLogs({ env, isTTY: process.stderr.isTTY === true });
|
|
237
|
+
if (built.kind === 'err') {
|
|
238
|
+
defaultPackServiceLogs().event('config-invalid', built.message, { problems: [built.message] });
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const logs = built.logs;
|
|
242
|
+
for (const problem of built.problems) logs.log.warn(problem);
|
|
222
243
|
const config = readPackServiceConfig(argv, env);
|
|
223
244
|
if (config.kind === 'err') {
|
|
224
|
-
|
|
245
|
+
logs.event('config-invalid', 'The pack service configuration is invalid', {
|
|
246
|
+
problems: config.problems,
|
|
247
|
+
});
|
|
225
248
|
return 1;
|
|
226
249
|
}
|
|
227
250
|
// The token is for the service's callers. The pack's code, loaded
|
|
228
251
|
// next, runs in this process and has no use for it — and a dependency
|
|
229
252
|
// that read it could call the pack's tools around the runtime.
|
|
230
253
|
Reflect.deleteProperty(process.env, 'KINDGI_PACK_SERVICE_TOKEN');
|
|
231
|
-
const started = await startPackService(config.value);
|
|
254
|
+
const started = await startPackService(config.value, logs);
|
|
232
255
|
if (started.kind === 'err') {
|
|
233
|
-
|
|
256
|
+
logs.event('boot-failed', 'The pack service failed to boot', { problems: started.problems });
|
|
234
257
|
return 1;
|
|
235
258
|
}
|
|
236
259
|
await new Promise<void>((stopped) => {
|
|
237
260
|
process.once('SIGTERM', () => {
|
|
238
|
-
|
|
261
|
+
logs.event('draining', 'Draining: finishing the calls in flight');
|
|
239
262
|
void started.value.stop().then(stopped);
|
|
240
263
|
});
|
|
241
264
|
});
|
|
242
|
-
|
|
265
|
+
logs.event('stopped', 'Stopped');
|
|
243
266
|
return 0;
|
|
244
267
|
}
|
|
245
268
|
|
|
246
|
-
function logJson(event: PackServiceLogEvent | Record<string, unknown>): void {
|
|
247
|
-
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
269
|
function describe(cause: unknown): string {
|
|
251
270
|
return cause instanceof Error ? cause.message : String(cause);
|
|
252
271
|
}
|