@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The pack service's log: `@kindgi/log` records on stderr, subsystem
|
|
6
|
+
* `pack` (the runtime writes the same schema). Two streams share the sink:
|
|
7
|
+
*
|
|
8
|
+
* - **`log`**: calls, and `ctx.log` beneath them (`pack.tool`,
|
|
9
|
+
* `pack.check`). `KINDGI_LOG_LEVEL` / `KINDGI_LOG_LEVELS` apply.
|
|
10
|
+
* - **`event`**: the lifecycle (`listening`, `boot-failed`, …), written
|
|
11
|
+
* whatever the levels, because a supervisor reads it to know the
|
|
12
|
+
* service is up. Its records carry the event's name as `event`, and as
|
|
13
|
+
* `kind` too, so a supervisor from before records (which reads bare
|
|
14
|
+
* `{"kind": …}` lines) still sees `listening`. `kind` stays through
|
|
15
|
+
* 0.1.x.
|
|
16
|
+
*
|
|
17
|
+
* The format is `KINDGI_LOG_FORMAT`'s, but `auto` is pretty only on a
|
|
18
|
+
* terminal: a supervisor pipes stderr, and dev mode (`KINDGI_DEV`) doesn't
|
|
19
|
+
* make it pretty, so a supervisor always gets JSON unless told otherwise.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
type LogFields,
|
|
24
|
+
type Logger,
|
|
25
|
+
createLogger,
|
|
26
|
+
loggerFromEnv,
|
|
27
|
+
resolveLogFormat,
|
|
28
|
+
} from '@kindgi/log';
|
|
29
|
+
|
|
30
|
+
/** Lifecycle events: always written, the supervisor's to read. */
|
|
31
|
+
export const PACK_SERVICE_EVENTS = [
|
|
32
|
+
'listening',
|
|
33
|
+
'boot-failed',
|
|
34
|
+
'config-invalid',
|
|
35
|
+
'draining',
|
|
36
|
+
'stopped',
|
|
37
|
+
] as const;
|
|
38
|
+
export type PackServiceEventKind = (typeof PACK_SERVICE_EVENTS)[number];
|
|
39
|
+
|
|
40
|
+
const EVENT_LEVEL = {
|
|
41
|
+
listening: 'info',
|
|
42
|
+
'boot-failed': 'error',
|
|
43
|
+
'config-invalid': 'error',
|
|
44
|
+
draining: 'info',
|
|
45
|
+
stopped: 'info',
|
|
46
|
+
} as const satisfies Record<PackServiceEventKind, 'info' | 'error'>;
|
|
47
|
+
|
|
48
|
+
export interface PackServiceLogs {
|
|
49
|
+
/** Calls, and `ctx.log` beneath them; the levels apply. */
|
|
50
|
+
readonly log: Logger;
|
|
51
|
+
/** A lifecycle event, written whatever the levels. */
|
|
52
|
+
event(kind: PackServiceEventKind, message: string, fields?: LogFields): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PackServiceLogsOutcome =
|
|
56
|
+
| { readonly kind: 'ok'; readonly logs: PackServiceLogs; readonly problems: readonly string[] }
|
|
57
|
+
| { readonly kind: 'err'; readonly message: string };
|
|
58
|
+
|
|
59
|
+
/** The pack service's logs from `KINDGI_LOG_*`; a bad setting is an error naming it. */
|
|
60
|
+
export function packServiceLogs(input: {
|
|
61
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
62
|
+
/** One line, no newline. Default stderr. */
|
|
63
|
+
readonly write?: (line: string) => void;
|
|
64
|
+
readonly isTTY?: boolean;
|
|
65
|
+
}): PackServiceLogsOutcome {
|
|
66
|
+
// Dev mode doesn't make the service's records pretty: its supervisor reads them.
|
|
67
|
+
const { KINDGI_DEV: _dev, ...env } = input.env;
|
|
68
|
+
const write = input.write ?? ((line: string) => void process.stderr.write(`${line}\n`));
|
|
69
|
+
const isTTY = input.isTTY ?? false;
|
|
70
|
+
// No unknown-subsystem check: pack code logs under its own names too
|
|
71
|
+
// (`kindgi.log.get_logger("billing")`, its own `@kindgi/log` loggers).
|
|
72
|
+
const built = loggerFromEnv({ env, write, isTTY });
|
|
73
|
+
if (built.kind === 'err') return built;
|
|
74
|
+
const format = resolveLogFormat(env.KINDGI_LOG_FORMAT, { isTTY }) ?? 'json';
|
|
75
|
+
const always = createLogger({
|
|
76
|
+
level: 'trace',
|
|
77
|
+
format,
|
|
78
|
+
color: format === 'pretty' && isTTY && (env.NO_COLOR ?? '') === '',
|
|
79
|
+
write,
|
|
80
|
+
subsystem: 'pack',
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
kind: 'ok',
|
|
84
|
+
problems: built.problems,
|
|
85
|
+
logs: {
|
|
86
|
+
log: built.logger.child({ subsystem: 'pack' }),
|
|
87
|
+
event(kind, message, fields = {}) {
|
|
88
|
+
always[EVENT_LEVEL[kind]](message, { ...fields, event: kind, kind });
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Logs for a caller that passes none: records on stderr, at the defaults. */
|
|
95
|
+
export function defaultPackServiceLogs(): PackServiceLogs {
|
|
96
|
+
const built = packServiceLogs({ env: {}, isTTY: false });
|
|
97
|
+
if (built.kind === 'err') throw new Error(built.message);
|
|
98
|
+
return built.logs;
|
|
99
|
+
}
|
|
@@ -29,7 +29,9 @@ import { timingSafeEqual } from 'node:crypto';
|
|
|
29
29
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
30
30
|
import { pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
-
import type
|
|
32
|
+
import { type Logger, childSpan, noopLogger, parseTraceparent } from '@kindgi/log';
|
|
33
|
+
|
|
34
|
+
import type { HandlerContext, HandlerError } from '../handler-runner.js';
|
|
33
35
|
import { runCheck, runHandler } from '../handler-runner.js';
|
|
34
36
|
import type { Index, IndexedGuardrail, IndexedTool } from '../kindgi-index.js';
|
|
35
37
|
import { type PackEnvCheck, missingPackEnv } from '../pack-env.js';
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
type CheckInvokeMessage,
|
|
38
40
|
PACK_HEADERS,
|
|
39
41
|
PACK_PROTOCOL_VERSION,
|
|
42
|
+
type PackCallContext,
|
|
40
43
|
type PackErrorCode,
|
|
41
44
|
type PackErrorMessage,
|
|
42
45
|
type PackResponse,
|
|
@@ -59,6 +62,12 @@ export interface PackServiceOptions {
|
|
|
59
62
|
/** Deadline when a call sends no `kindgi-timeout-ms`. Default 120 s. */
|
|
60
63
|
readonly defaultTimeoutMs?: number;
|
|
61
64
|
readonly logger?: (event: PackServiceLogEvent) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Where the service's records go (`@kindgi/log`, subsystem `pack`): a
|
|
67
|
+
* record per call, with the call's ids and trace, and each handler's
|
|
68
|
+
* `ctx.log` beneath it. Default: none. `logger` still gets its events.
|
|
69
|
+
*/
|
|
70
|
+
readonly log?: Logger;
|
|
62
71
|
/**
|
|
63
72
|
* The process environment the index's `env.required` names are checked
|
|
64
73
|
* against, once, at creation. Default `process.env`.
|
|
@@ -115,6 +124,7 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
115
124
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
116
125
|
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
117
126
|
const logger = options.logger ?? (() => undefined);
|
|
127
|
+
const log = options.log ?? noopLogger;
|
|
118
128
|
const importModule =
|
|
119
129
|
options.importModule ?? ((p: string) => import(pathToFileURL(p).href) as Promise<unknown>);
|
|
120
130
|
const expectedToken = Buffer.from(options.token);
|
|
@@ -138,7 +148,13 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
138
148
|
|
|
139
149
|
const envCheck = options.envCheck ?? 'strict';
|
|
140
150
|
const missingEnv = missingPackEnv(options.index.env, options.env ?? process.env);
|
|
141
|
-
if (missingEnv.length > 0)
|
|
151
|
+
if (missingEnv.length > 0) {
|
|
152
|
+
logger({ kind: 'missing-env', check: envCheck, names: missingEnv });
|
|
153
|
+
log.warn(
|
|
154
|
+
`The pack's env.required ${missingEnv.length === 1 ? 'name has' : 'names have'} no value: ${missingEnv.join(', ')}`,
|
|
155
|
+
{ event: 'missing-env', kind: 'missing-env', check: envCheck, names: missingEnv },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
142
158
|
|
|
143
159
|
/** Why the service can't take calls now, or `undefined` when it can. */
|
|
144
160
|
function notReady(): Reply | undefined {
|
|
@@ -239,7 +255,11 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
239
255
|
};
|
|
240
256
|
}
|
|
241
257
|
|
|
242
|
-
function callTool(
|
|
258
|
+
function callTool(
|
|
259
|
+
message: ToolInvokeMessage,
|
|
260
|
+
signal: AbortSignal,
|
|
261
|
+
callLog: Logger,
|
|
262
|
+
): Promise<PackResponse> {
|
|
243
263
|
const found = toolFor(message.tool);
|
|
244
264
|
if ('refused' in found) return Promise.resolve(found.refused);
|
|
245
265
|
const { tool } = found;
|
|
@@ -251,7 +271,7 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
251
271
|
outputSchema: tool.output,
|
|
252
272
|
},
|
|
253
273
|
input: message.input,
|
|
254
|
-
ctx:
|
|
274
|
+
ctx: handlerContext(message.ctx, signal, callLog),
|
|
255
275
|
importHandler: (p) => importModule(p) as never,
|
|
256
276
|
}).then((r) =>
|
|
257
277
|
r.kind === 'ok'
|
|
@@ -270,7 +290,11 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
270
290
|
);
|
|
271
291
|
}
|
|
272
292
|
return runCheck({
|
|
273
|
-
check: {
|
|
293
|
+
check: {
|
|
294
|
+
id: message.check.id,
|
|
295
|
+
modulePath: options.resolveModule(guardrail.checkModulePath),
|
|
296
|
+
...(guardrail.configSchema !== undefined && { configSchema: guardrail.configSchema }),
|
|
297
|
+
},
|
|
274
298
|
config: message.config,
|
|
275
299
|
trace: message.trace,
|
|
276
300
|
abortSignal: signal,
|
|
@@ -349,24 +373,38 @@ export function createPackService(options: PackServiceOptions): PackService {
|
|
|
349
373
|
});
|
|
350
374
|
|
|
351
375
|
const started = Date.now();
|
|
376
|
+
// The call's ids and trace, on its record and on the handler's `ctx.log`.
|
|
377
|
+
const call = log.child({
|
|
378
|
+
...callIds(message.kind === 'invoke' ? message.ctx : undefined),
|
|
379
|
+
...callTrace(req.headers[PACK_HEADERS.traceparent]),
|
|
380
|
+
target,
|
|
381
|
+
...(message.kind === 'invoke' ? { toolId: id } : { checkId: id }),
|
|
382
|
+
});
|
|
352
383
|
const work =
|
|
353
384
|
message.kind === 'invoke'
|
|
354
|
-
? callTool(message, controller.signal)
|
|
385
|
+
? callTool(message, controller.signal, call.child({ subsystem: `pack.${target}` }))
|
|
355
386
|
: callCheck(message, controller.signal);
|
|
356
387
|
const outcome = await Promise.race([work, aborted(controller.signal, id, target)]);
|
|
357
388
|
clearTimeout(timer);
|
|
358
389
|
if (controller.signal.aborted) {
|
|
359
|
-
void work.then(() =>
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
void work.then(() => {
|
|
391
|
+
const afterMs = Date.now() - started;
|
|
392
|
+
logger({ kind: 'handler-finished-late', target, id, afterMs });
|
|
393
|
+
call.warn(`${target} ${id} finished ${afterMs} ms after its call ended`, {
|
|
394
|
+
event: 'handler-finished-late',
|
|
395
|
+
kind: 'handler-finished-late',
|
|
396
|
+
afterMs,
|
|
397
|
+
});
|
|
398
|
+
});
|
|
362
399
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
400
|
+
const durationMs = Date.now() - started;
|
|
401
|
+
const result = outcome.kind === 'error' ? outcome.code : 'ok';
|
|
402
|
+
logger({ kind: 'call', target, id, durationMs, outcome: result });
|
|
403
|
+
call[result === 'ok' ? 'info' : 'warn'](
|
|
404
|
+
`${target} ${id} ${result} ${durationMs}ms`,
|
|
405
|
+
{ event: 'call', kind: 'call', id, outcome: result, durationMs },
|
|
406
|
+
{ inMessage: ['target', 'id', 'outcome', 'durationMs'] },
|
|
407
|
+
);
|
|
370
408
|
return outcome;
|
|
371
409
|
}
|
|
372
410
|
|
|
@@ -509,3 +547,45 @@ function describe(cause: unknown): string {
|
|
|
509
547
|
function toolKey(id: string, version: string | undefined): string {
|
|
510
548
|
return version === undefined ? id : `${id}@${version}`;
|
|
511
549
|
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The context a handler gets: the call's own (`PackCallContext`) and its
|
|
553
|
+
* abort signal, with `log` and `secrets` there but not enumerable, so
|
|
554
|
+
* printing, spreading or serializing a context never shows a secret.
|
|
555
|
+
*/
|
|
556
|
+
function handlerContext(wire: PackCallContext, signal: AbortSignal, log: Logger): HandlerContext {
|
|
557
|
+
const { secrets, ...rest } = wire;
|
|
558
|
+
const ctx = { ...rest, abortSignal: signal };
|
|
559
|
+
if (secrets !== undefined) {
|
|
560
|
+
Object.defineProperty(ctx, 'secrets', { value: secrets, enumerable: false });
|
|
561
|
+
}
|
|
562
|
+
Object.defineProperty(ctx, 'log', { value: log, enumerable: false });
|
|
563
|
+
return ctx as HandlerContext;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** The ids a call's records carry, from its context. */
|
|
567
|
+
function callIds(ctx: PackCallContext | undefined): Record<string, string> {
|
|
568
|
+
if (ctx === undefined) return {};
|
|
569
|
+
const ids: Record<string, string> = {};
|
|
570
|
+
for (const key of ['tenantId', 'projectId', 'orgId', 'runId', 'requestId'] as const) {
|
|
571
|
+
const value: unknown = ctx[key];
|
|
572
|
+
if (typeof value === 'string' && value !== '') ids[key] = value;
|
|
573
|
+
}
|
|
574
|
+
return ids;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The call's trace: the caller's trace id, and a span of this call's own
|
|
579
|
+
* whose parent is the caller's. None when the caller sent no
|
|
580
|
+
* `traceparent` (an older runtime): a fresh trace would join nothing.
|
|
581
|
+
*/
|
|
582
|
+
function callTrace(header: string | string[] | undefined): Record<string, string> {
|
|
583
|
+
const incoming = parseTraceparent(Array.isArray(header) ? header[0] : header);
|
|
584
|
+
if (incoming === undefined) return {};
|
|
585
|
+
const span = childSpan({
|
|
586
|
+
traceId: incoming.traceId,
|
|
587
|
+
spanId: incoming.parentSpanId,
|
|
588
|
+
flags: incoming.flags,
|
|
589
|
+
});
|
|
590
|
+
return { traceId: span.traceId, spanId: span.spanId };
|
|
591
|
+
}
|
|
@@ -50,6 +50,7 @@ export interface PackServiceSupervisorOptions {
|
|
|
50
50
|
*
|
|
51
51
|
* - Node: `[process.execPath, '--enable-source-maps', <@kindgi/handler-runtime/pack-service-main>]`
|
|
52
52
|
* - Python: `[<the pack's python>, '-m', 'kindgi.pack', 'serve']`
|
|
53
|
+
* - Java and Scala: `['sh', <kindgi-pack-java>, '-cp', <classpath> (or `@<argfile>`), 'com.kindgi.pack.Main', 'serve']`
|
|
53
54
|
*/
|
|
54
55
|
readonly command: readonly [string, ...string[]];
|
|
55
56
|
/** Where the index's module paths resolve — the pack directory. */
|
|
@@ -102,8 +103,16 @@ export type PackServiceSupervisorEvent =
|
|
|
102
103
|
| { readonly kind: 'exited'; readonly code: number | null; readonly signal: string | null }
|
|
103
104
|
| { readonly kind: 'restarting'; readonly attempt: number }
|
|
104
105
|
| { readonly kind: 'gave-up'; readonly attempts: number }
|
|
105
|
-
/**
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* A record the service wrote (a call, draining, an author's `ctx.log`
|
|
108
|
+
* line, …), or a bare event from an older service: `event` as
|
|
109
|
+
* `serviceEvent` reads it, `line` as written.
|
|
110
|
+
*/
|
|
111
|
+
| {
|
|
112
|
+
readonly kind: 'log';
|
|
113
|
+
readonly event: Readonly<Record<string, unknown>>;
|
|
114
|
+
readonly line: string;
|
|
115
|
+
};
|
|
107
116
|
|
|
108
117
|
export interface BootFailure {
|
|
109
118
|
readonly problems: readonly string[];
|
|
@@ -122,6 +131,8 @@ export interface PackRelayCall {
|
|
|
122
131
|
readonly requestId?: string;
|
|
123
132
|
/** The caller's protocol version (`kindgi-protocol`), passed on as is. */
|
|
124
133
|
readonly protocol?: string;
|
|
134
|
+
/** The caller's W3C trace context (`traceparent`), passed on as is, so the child's records carry its trace. */
|
|
135
|
+
readonly traceparent?: string;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
/**
|
|
@@ -223,7 +234,13 @@ export function createPackServiceSupervisor(
|
|
|
223
234
|
let front: { readonly server: Server; readonly url: string; readonly port: number } | undefined;
|
|
224
235
|
|
|
225
236
|
async function boot(indexPath: string): Promise<Result<Child, BootFailure>> {
|
|
226
|
-
|
|
237
|
+
// JSON whatever the pack's env says: this process reads the child's records.
|
|
238
|
+
const env = {
|
|
239
|
+
...(await options.env()),
|
|
240
|
+
KINDGI_PACK_SERVICE_TOKEN: token,
|
|
241
|
+
PORT: '0',
|
|
242
|
+
KINDGI_LOG_FORMAT: 'json',
|
|
243
|
+
};
|
|
227
244
|
const [program, ...args] = options.command;
|
|
228
245
|
const child = spawn(
|
|
229
246
|
program,
|
|
@@ -263,7 +280,7 @@ export function createPackServiceSupervisor(
|
|
|
263
280
|
} else if (event.kind === 'boot-failed' || event.kind === 'config-invalid') {
|
|
264
281
|
problems.push(...problemsOf(event));
|
|
265
282
|
} else {
|
|
266
|
-
emit({ kind: 'log', event });
|
|
283
|
+
emit({ kind: 'log', event, line });
|
|
267
284
|
}
|
|
268
285
|
});
|
|
269
286
|
child.once('error', (cause) => fail([cause.message]));
|
|
@@ -485,6 +502,7 @@ function childHeaders(token: string, call: PackRelayCall): Record<string, string
|
|
|
485
502
|
...(call.runId !== undefined && { [PACK_HEADERS.runId]: call.runId }),
|
|
486
503
|
...(call.requestId !== undefined && { [PACK_HEADERS.requestId]: call.requestId }),
|
|
487
504
|
...(call.protocol !== undefined && { [PACK_HEADERS.protocol]: call.protocol }),
|
|
505
|
+
...(call.traceparent !== undefined && { [PACK_HEADERS.traceparent]: call.traceparent }),
|
|
488
506
|
};
|
|
489
507
|
}
|
|
490
508
|
|
|
@@ -518,6 +536,7 @@ function relayCall(
|
|
|
518
536
|
const runId = header(req, PACK_HEADERS.runId);
|
|
519
537
|
const requestId = header(req, PACK_HEADERS.requestId);
|
|
520
538
|
const protocol = header(req, PACK_HEADERS.protocol);
|
|
539
|
+
const traceparent = header(req, PACK_HEADERS.traceparent);
|
|
521
540
|
return {
|
|
522
541
|
route,
|
|
523
542
|
signal,
|
|
@@ -526,6 +545,7 @@ function relayCall(
|
|
|
526
545
|
...(runId !== undefined && { runId }),
|
|
527
546
|
...(requestId !== undefined && { requestId }),
|
|
528
547
|
...(protocol !== undefined && { protocol }),
|
|
548
|
+
...(traceparent !== undefined && { traceparent }),
|
|
529
549
|
};
|
|
530
550
|
}
|
|
531
551
|
|
|
@@ -620,21 +640,58 @@ function problemsOf(event: Readonly<Record<string, unknown>>): readonly string[]
|
|
|
620
640
|
return Array.isArray(event.problems) ? event.problems.map(String) : [];
|
|
621
641
|
}
|
|
622
642
|
|
|
623
|
-
/**
|
|
624
|
-
|
|
643
|
+
/** What a pack service from before records wrote as bare `{"kind": …}` lines. */
|
|
644
|
+
const LEGACY_EVENTS: ReadonlySet<string> = new Set([
|
|
645
|
+
'listening',
|
|
646
|
+
'boot-failed',
|
|
647
|
+
'config-invalid',
|
|
648
|
+
'missing-env',
|
|
649
|
+
'draining',
|
|
650
|
+
'stopped',
|
|
651
|
+
'call',
|
|
652
|
+
'handler-finished-late',
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* One of the service's own log lines, as an event with a `kind`:
|
|
657
|
+
*
|
|
658
|
+
* - a record (`@kindgi/log`: `time`, `level`, `subsystem`, `message`).
|
|
659
|
+
* The service's own, subsystem `pack`, carry their event as `event`
|
|
660
|
+
* (`listening`, `call`, …), which becomes `kind`. Any other record, an
|
|
661
|
+
* author's `ctx.log` (`pack.tool`) say, comes back with `kind: 'record'`,
|
|
662
|
+
* to show, never to act on;
|
|
663
|
+
* - a bare `{"kind": …}` line from a pack service from before records,
|
|
664
|
+
* for the events it wrote. Any other JSON line is the pack's own
|
|
665
|
+
* output, shown as it is.
|
|
666
|
+
*/
|
|
667
|
+
export function serviceEvent(
|
|
625
668
|
line: string,
|
|
626
669
|
): (Record<string, unknown> & { readonly kind: string }) | undefined {
|
|
627
670
|
if (!line.startsWith('{')) return undefined;
|
|
671
|
+
let parsed: unknown;
|
|
628
672
|
try {
|
|
629
|
-
|
|
630
|
-
return parsed !== null &&
|
|
631
|
-
typeof parsed === 'object' &&
|
|
632
|
-
typeof (parsed as { kind?: unknown }).kind === 'string'
|
|
633
|
-
? (parsed as Record<string, unknown> & { readonly kind: string })
|
|
634
|
-
: undefined;
|
|
673
|
+
parsed = JSON.parse(line);
|
|
635
674
|
} catch {
|
|
636
675
|
return undefined;
|
|
637
676
|
}
|
|
677
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
|
|
678
|
+
const fields = parsed as Record<string, unknown>;
|
|
679
|
+
if (isRecord(fields)) {
|
|
680
|
+
const own = fields.subsystem === 'pack' && typeof fields.event === 'string';
|
|
681
|
+
return { ...fields, kind: own ? (fields.event as string) : 'record' };
|
|
682
|
+
}
|
|
683
|
+
return typeof fields.kind === 'string' && LEGACY_EVENTS.has(fields.kind)
|
|
684
|
+
? (fields as Record<string, unknown> & { readonly kind: string })
|
|
685
|
+
: undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isRecord(fields: Record<string, unknown>): boolean {
|
|
689
|
+
return (
|
|
690
|
+
typeof fields.time === 'string' &&
|
|
691
|
+
typeof fields.level === 'string' &&
|
|
692
|
+
typeof fields.subsystem === 'string' &&
|
|
693
|
+
typeof fields.message === 'string'
|
|
694
|
+
);
|
|
638
695
|
}
|
|
639
696
|
|
|
640
697
|
function describe(cause: unknown): string {
|
package/src/protocol.ts
CHANGED
|
@@ -32,8 +32,13 @@ export interface PackCallContext {
|
|
|
32
32
|
readonly orgId?: string;
|
|
33
33
|
/** The individual call — e.g. the model's tool-call id. */
|
|
34
34
|
readonly requestId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The env values the tool declares (`needsSpec.env`), resolved for this
|
|
37
|
+
* call: project, else org, else tenant (protocol 2.5.0). Strings.
|
|
38
|
+
*/
|
|
35
39
|
readonly env?: Readonly<Record<string, unknown>>;
|
|
36
40
|
readonly secrets?: Readonly<Record<string, unknown>>;
|
|
41
|
+
/** Reserved: no runtime sends it yet. */
|
|
37
42
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
38
43
|
/** The calling agent version's settings blocks' values, by block id (protocol 2.4.0). */
|
|
39
44
|
readonly settings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
@@ -219,8 +224,8 @@ export const PACK_HEADERS = {
|
|
|
219
224
|
durationMs: 'kindgi-duration-ms',
|
|
220
225
|
/**
|
|
221
226
|
* W3C Trace Context: the run's trace, so a pack service's records carry
|
|
222
|
-
* the same `traceId` as the runtime's.
|
|
223
|
-
* a pack service that doesn't read it ignores it.
|
|
227
|
+
* the same `traceId` as the runtime's. Optional: runtimes from 0.1.5 send
|
|
228
|
+
* it on every call; a pack service that doesn't read it ignores it.
|
|
224
229
|
*/
|
|
225
230
|
traceparent: 'traceparent',
|
|
226
231
|
} as const;
|