bazis 0.99.4 → 0.99.6
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/core/agent/AgentRuntime.ts +12 -0
- package/core/agent/ModelProvider.ts +6 -0
- package/core/agent/README.md +5 -0
- package/core/agent/errors.ts +11 -0
- package/core/agent/index.ts +1 -0
- package/core/agent/internal/AgentExecutionDriver.ts +2 -2
- package/core/di/extensions/hosted-service.ts +2 -0
- package/core/infra/InfraLifecycle.ts +17 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/llmRouter.ts +23 -4
- package/core/infra/connectors/openaiCompatible.ts +2 -1
- package/core/infra/connectors/opensearch.ts +8 -4
- package/core/infra/connectors/postgres.ts +9 -4
- package/core/infra/connectors/redis.ts +41 -7
- package/core/infra/index.ts +3 -3
- package/core/kernel/Kernel.ts +1 -1
- package/core/kernel/LifecycleCoordinator.ts +9 -0
- package/core/kernel/SupervisedHostedService.ts +5 -0
- package/core/kernel/errors/StartupTimeoutError.ts +8 -5
- package/index.ts +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createToken, type ServiceProvider } from "../di";
|
|
2
2
|
import { modelValidatorAdapter } from "../../library/validation";
|
|
3
|
+
import { AgentSetupError } from "./errors";
|
|
3
4
|
import type { AgentDataDefinition } from "./AgentDataDefinition";
|
|
4
5
|
import { AgentRegistry, type ToolDefinition } from "./AgentRegistry";
|
|
5
6
|
import {
|
|
@@ -62,6 +63,15 @@ export class AgentRuntime {
|
|
|
62
63
|
return this.catalog.listTools();
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/** A profile the model provider does not serve fails before the model is called. */
|
|
67
|
+
private assertModelProfile(agentName: string, profile: string | undefined): void {
|
|
68
|
+
if (profile === undefined || typeof this.modelProvider.modelProfiles !== "function") return;
|
|
69
|
+
const configured = this.modelProvider.modelProfiles();
|
|
70
|
+
if (!configured.includes(profile)) {
|
|
71
|
+
throw new AgentSetupError(`Agent "${agentName}": model profile "${profile}" is not configured (configured: ${[...configured].sort().join(", ")}).`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
/**
|
|
66
76
|
* Checks a definition the way `run` does, without calling the model: an
|
|
67
77
|
* invalid field or a tool name not registered in `@Module.tools` throws
|
|
@@ -69,6 +79,7 @@ export class AgentRuntime {
|
|
|
69
79
|
*/
|
|
70
80
|
check(definition: AgentDataDefinition): void {
|
|
71
81
|
AgentRegistry.fromDefinition(definition, this.catalog.listTools());
|
|
82
|
+
this.assertModelProfile(definition.name, definition.modelProfile);
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -78,6 +89,7 @@ export class AgentRuntime {
|
|
|
78
89
|
*/
|
|
79
90
|
async run(definition: AgentDataDefinition, options: AgentRuntimeInvokeOptions = {}): Promise<AgentRuntimeResult> {
|
|
80
91
|
const registry = AgentRegistry.fromDefinition(definition, this.catalog.listTools());
|
|
92
|
+
this.assertModelProfile(definition.name, options.modelProfile ?? definition.modelProfile);
|
|
81
93
|
const driver = new AgentExecutionDriver(this.services, registry, this.modelProvider, this.options);
|
|
82
94
|
return driver.invoke(definition.name, options);
|
|
83
95
|
}
|
|
@@ -16,6 +16,12 @@ export interface AgentModelProvider {
|
|
|
16
16
|
request: AgentModelRequest,
|
|
17
17
|
context: AgentModelProviderContext,
|
|
18
18
|
): AgentModelResponse | Promise<AgentModelResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* Model profiles this provider serves, when it routes by profile
|
|
21
|
+
* (`llmRouter`). AgentRuntime rejects an agent naming another profile before
|
|
22
|
+
* the model is called. Without it any profile is passed through.
|
|
23
|
+
*/
|
|
24
|
+
modelProfiles?(): readonly string[];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export const AGENT_MODEL_PROVIDER = createToken<AgentModelProvider>("AgentModelProvider");
|
package/core/agent/README.md
CHANGED
|
@@ -143,6 +143,11 @@ await this.agents.run(support, {
|
|
|
143
143
|
conversation message when trimming old history. Provider requests include
|
|
144
144
|
`metadata.agentContext.trace` so context assembly is inspectable.
|
|
145
145
|
|
|
146
|
+
With `llmRouter`, `check` and `run` reject a `modelProfile` the router does
|
|
147
|
+
not serve; a fallback to another profile is logged as `LLM model profile
|
|
148
|
+
failed, using fallback`. A model provider that gives up after its own deadline
|
|
149
|
+
throws `AgentModelTimeoutError`, and the run reports `AGENT_PROVIDER_TIMEOUT`.
|
|
150
|
+
|
|
146
151
|
LLM clients are registered by infra connectors. Agents name a model profile;
|
|
147
152
|
infra owns provider URLs, model ids, limits and fallback:
|
|
148
153
|
|
package/core/agent/errors.ts
CHANGED
|
@@ -47,6 +47,17 @@ export class AgentToolPreCommitError extends ToolError {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A model provider gave up on a request after its own deadline (for example
|
|
52
|
+
* the connector's `timeoutMs`). The run reports it as AGENT_PROVIDER_TIMEOUT.
|
|
53
|
+
*/
|
|
54
|
+
export class AgentModelTimeoutError extends Error {
|
|
55
|
+
constructor(message: string) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "AgentModelTimeoutError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
/** Error thrown when the agent runtime is configured incorrectly. */
|
|
51
62
|
export class AgentRuntimeError extends Error {
|
|
52
63
|
constructor(message: string) {
|
package/core/agent/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ServiceProvider } from "../../di";
|
|
|
4
4
|
import { getGeneratedOpenApiMetadata } from "../../http/OpenApi/generatedOpenApiRegistry";
|
|
5
5
|
import { redactSensitive, redactSensitiveText } from "../../../library/redaction";
|
|
6
6
|
import type { Logger } from "../../kernel/logging/Logger";
|
|
7
|
-
import { AgentRuntimeError } from "../errors";
|
|
7
|
+
import { AgentModelTimeoutError, AgentRuntimeError } from "../errors";
|
|
8
8
|
import { validateAgentJsonSchema } from "./AgentJsonSchema";
|
|
9
9
|
import type { AgentDefinition, AgentRegistry } from "../AgentRegistry";
|
|
10
10
|
import {
|
|
@@ -1137,7 +1137,7 @@ export class AgentExecutionDriver {
|
|
|
1137
1137
|
};
|
|
1138
1138
|
}
|
|
1139
1139
|
} catch (error) {
|
|
1140
|
-
const code = error instanceof AgentRuntimeTimeoutError
|
|
1140
|
+
const code = error instanceof AgentRuntimeTimeoutError || error instanceof AgentModelTimeoutError
|
|
1141
1141
|
? "AGENT_PROVIDER_TIMEOUT"
|
|
1142
1142
|
: error instanceof AgentRuntimeAbortError
|
|
1143
1143
|
? "AGENT_ABORTED"
|
|
@@ -38,6 +38,8 @@ export interface HostedService {
|
|
|
38
38
|
readonly planValidator?: HostedServicePlanValidator;
|
|
39
39
|
/** Receives the application logger before start, when one is registered. */
|
|
40
40
|
useDiagnostics?(diagnostics: HostedServiceDiagnostics): void;
|
|
41
|
+
/** Name in startup diagnostics, such as `infra:db`; the class name by default. */
|
|
42
|
+
readonly diagnosticName?: string;
|
|
41
43
|
start(signal?: AbortSignal): void | Promise<void>;
|
|
42
44
|
stop(): void | Promise<void>;
|
|
43
45
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HostedService } from "../di";
|
|
1
|
+
import type { HostedService, HostedServiceDiagnostics } from "../di";
|
|
2
2
|
import { InfraError, type InfraConnector } from "./InfraConnector";
|
|
3
3
|
import { bindConnectorLifecycle } from "./connectorIdentity";
|
|
4
4
|
import { awaitAbortable } from "../kernel/internal/awaitAbortable";
|
|
@@ -17,11 +17,14 @@ const DEFAULT_INFRA_PHASE = -100;
|
|
|
17
17
|
*/
|
|
18
18
|
export class InfraLifecycle<TClient> implements HostedService {
|
|
19
19
|
public readonly phase: number;
|
|
20
|
+
/** The manifest key, as in `/health`: `infra:db`. */
|
|
21
|
+
public readonly diagnosticName: string;
|
|
20
22
|
private client?: TClient;
|
|
21
23
|
private created = false;
|
|
22
24
|
private disposed = false;
|
|
23
25
|
private startPromise?: Promise<void>;
|
|
24
26
|
private disposePromise?: Promise<void>;
|
|
27
|
+
private diagnostics?: HostedServiceDiagnostics;
|
|
25
28
|
|
|
26
29
|
public constructor(
|
|
27
30
|
private readonly name: string,
|
|
@@ -32,6 +35,7 @@ export class InfraLifecycle<TClient> implements HostedService {
|
|
|
32
35
|
this.client = client;
|
|
33
36
|
this.created = client !== undefined;
|
|
34
37
|
this.phase = connector.phase ?? DEFAULT_INFRA_PHASE;
|
|
38
|
+
this.diagnosticName = `infra:${name}`;
|
|
35
39
|
bindConnectorLifecycle(connector, this);
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -40,10 +44,22 @@ export class InfraLifecycle<TClient> implements HostedService {
|
|
|
40
44
|
if (!this.created) {
|
|
41
45
|
this.client = this.connector.create(this.configs);
|
|
42
46
|
this.created = true;
|
|
47
|
+
this.forwardDiagnostics();
|
|
43
48
|
}
|
|
44
49
|
return this.client as TClient;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** The application logger reaches a client that reports on its own, such as `llmRouter` falling back. */
|
|
53
|
+
public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
|
|
54
|
+
this.diagnostics = diagnostics;
|
|
55
|
+
if (this.created) this.forwardDiagnostics();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private forwardDiagnostics(): void {
|
|
59
|
+
const client = this.client as { useDiagnostics?: (diagnostics: HostedServiceDiagnostics) => void } | undefined;
|
|
60
|
+
if (this.diagnostics !== undefined && typeof client?.useDiagnostics === "function") client.useDiagnostics(this.diagnostics);
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
public start(signal?: AbortSignal): Promise<void> {
|
|
48
64
|
this.startPromise ??= Promise.resolve().then(() => this.connect(signal));
|
|
49
65
|
return this.startPromise;
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.99.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.99.6" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AppConfig } from "../../kernel";
|
|
2
|
+
import type { HostedServiceDiagnostics } from "../../di";
|
|
3
|
+
import { redactSensitive } from "../../../library/redaction";
|
|
2
4
|
import {
|
|
3
5
|
AGENT_MODEL_PROVIDER,
|
|
6
|
+
AgentModelTimeoutError,
|
|
4
7
|
AgentRuntime,
|
|
5
8
|
agentModelRequest,
|
|
6
9
|
type AgentModelProvider,
|
|
@@ -194,11 +197,13 @@ export class LlmModelRouter implements AgentModelProvider {
|
|
|
194
197
|
private initializePromise?: Promise<void>;
|
|
195
198
|
private disposePromise?: Promise<void>;
|
|
196
199
|
private disposed = false;
|
|
200
|
+
private diagnostics?: HostedServiceDiagnostics;
|
|
197
201
|
|
|
198
202
|
public constructor(
|
|
199
203
|
profiles: readonly RuntimeProfile[],
|
|
200
204
|
private readonly defaultProfile: string,
|
|
201
205
|
private readonly factories: readonly (() => RuntimeProfile)[] = [],
|
|
206
|
+
private readonly profileNames: readonly string[] = profiles.map((profile) => profile.name),
|
|
202
207
|
) {
|
|
203
208
|
for (let index = 0; index < profiles.length; index += 1) {
|
|
204
209
|
const profile = profiles[index] as RuntimeProfile;
|
|
@@ -213,6 +218,16 @@ export class LlmModelRouter implements AgentModelProvider {
|
|
|
213
218
|
return this.completeWithProfile(request, context, requested, new Set<string>());
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
/** Profile names an agent may use in `modelProfile`. */
|
|
222
|
+
public modelProfiles(): readonly string[] {
|
|
223
|
+
return this.profileNames;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The application logger: a fallback is reported, not silent. */
|
|
227
|
+
public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
|
|
228
|
+
this.diagnostics = diagnostics;
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
public connect(signal?: AbortSignal): Promise<void> {
|
|
217
232
|
if (this.disposed) {
|
|
218
233
|
return Promise.reject(new InfraError("LLM model router is already disposed."));
|
|
@@ -363,11 +378,15 @@ export class LlmModelRouter implements AgentModelProvider {
|
|
|
363
378
|
// A second model must not silently append a new answer to already visible text.
|
|
364
379
|
if (fallback !== undefined && !emittedText) {
|
|
365
380
|
if (context.signal.aborted) throw new LlmModelRouterError("LLM model request was cancelled before provider dispatch.");
|
|
381
|
+
this.diagnostics?.warn("LLM model profile failed, using fallback", {
|
|
382
|
+
profile: profileName, fallback, agent: context.agentName, invocationId: context.invocationId, error: redactSensitive(error),
|
|
383
|
+
});
|
|
366
384
|
return this.completeWithProfile(request, context, fallback, visited);
|
|
367
385
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
386
|
+
const message = `LLM model profile "${profileName}" failed: ${errorMessageOf(error)}`;
|
|
387
|
+
// Keep a timeout a timeout: the run reports it as AGENT_PROVIDER_TIMEOUT.
|
|
388
|
+
if (error instanceof AgentModelTimeoutError) throw new AgentModelTimeoutError(message);
|
|
389
|
+
throw new LlmModelRouterError(message);
|
|
371
390
|
} finally {
|
|
372
391
|
acceptingText = false;
|
|
373
392
|
}
|
|
@@ -432,7 +451,7 @@ export function llmRouter(
|
|
|
432
451
|
options: profile.options,
|
|
433
452
|
});
|
|
434
453
|
});
|
|
435
|
-
return new LlmModelRouter([], defaultProfile, factories);
|
|
454
|
+
return new LlmModelRouter([], defaultProfile, factories, entries.map(([name]) => name));
|
|
436
455
|
},
|
|
437
456
|
connect(client, signal) {
|
|
438
457
|
return client instanceof LlmModelRouter ? client.connect(signal) : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AgentModelTimeoutError,
|
|
2
3
|
agentMessage,
|
|
3
4
|
agentModelResponse,
|
|
4
5
|
agentToolCall,
|
|
@@ -490,7 +491,7 @@ export class OpenAiCompatibleModelProvider implements AgentModelProvider {
|
|
|
490
491
|
const detach = attachAbortForwarding(signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal, abort);
|
|
491
492
|
const timeout = this.requestTimeoutMs === undefined
|
|
492
493
|
? undefined
|
|
493
|
-
: setTimeout(() => abort.abort(new
|
|
494
|
+
: setTimeout(() => abort.abort(new AgentModelTimeoutError("OpenAI-compatible request timed out.")), this.requestTimeoutMs);
|
|
494
495
|
try {
|
|
495
496
|
abort.signal.throwIfAborted();
|
|
496
497
|
const response = await this.fetcher(`${this.baseUrl}${path}`, {
|
|
@@ -134,8 +134,12 @@ export class OpenSearchClient {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* OpenSearch client of the `openSearchConnect()` connector, injected by type:
|
|
139
|
+
* `constructor(private readonly search: IOpenSearch)`.
|
|
140
|
+
*/
|
|
141
|
+
export interface IOpenSearch extends OpenSearchClient {}
|
|
142
|
+
export const IOpenSearch = createToken<IOpenSearch>("IOpenSearch") as InjectionToken<OpenSearchClient>;
|
|
139
143
|
|
|
140
144
|
/**
|
|
141
145
|
* Config interface required by the OpenSearch connector. The search subsystem
|
|
@@ -278,7 +282,7 @@ async function readBoundedText(response: Response, maxBytes: number, label: stri
|
|
|
278
282
|
* });
|
|
279
283
|
* @Infra({ search: openSearchConnect(searchConfig) })
|
|
280
284
|
* export class AppInfra {}
|
|
281
|
-
* //
|
|
285
|
+
* // by type: class SearchService { constructor(private readonly search: IOpenSearch) {} }
|
|
282
286
|
* ```
|
|
283
287
|
*/
|
|
284
288
|
export function openSearchConnect<T extends OpenSearchConfigShape>(
|
|
@@ -286,7 +290,7 @@ export function openSearchConnect<T extends OpenSearchConfigShape>(
|
|
|
286
290
|
options: OpenSearchConnectorOptions = {},
|
|
287
291
|
): InfraConnector<OpenSearchClient> {
|
|
288
292
|
return {
|
|
289
|
-
token: options.token ??
|
|
293
|
+
token: options.token ?? IOpenSearch,
|
|
290
294
|
config,
|
|
291
295
|
create(configs) {
|
|
292
296
|
return new OpenSearchClient(buildOpenSearchOptions(config, options, configs));
|
|
@@ -4,8 +4,13 @@ import type { AppConfig, ConfigRegistry, Secret } from "../../kernel";
|
|
|
4
4
|
import { reader, requireValue } from "../connectorConfig";
|
|
5
5
|
import { errorMessage, InfraError, type InfraConnector } from "../InfraConnector";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL client (native Bun `SQL`, no external dependencies) of the
|
|
9
|
+
* `postgres()` connector, injected by type:
|
|
10
|
+
* `constructor(private readonly sql: IPostgres)`.
|
|
11
|
+
*/
|
|
12
|
+
export interface IPostgres extends SQL {}
|
|
13
|
+
export const IPostgres = createToken<IPostgres>("IPostgres") as InjectionToken<SQL>;
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Config interface required by the PostgreSQL connector. The database subsystem
|
|
@@ -69,7 +74,7 @@ export function postgresConnectionOptions(config: AppConfig<PostgresConfigShape>
|
|
|
69
74
|
* });
|
|
70
75
|
* @Infra({ db: postgres(dbConfig) })
|
|
71
76
|
* export class AppInfra {}
|
|
72
|
-
* //
|
|
77
|
+
* // by type: class ReportsService { constructor(private readonly sql: IPostgres) {} }
|
|
73
78
|
* ```
|
|
74
79
|
*/
|
|
75
80
|
export function postgres<T extends PostgresConfigShape>(config: AppConfig<T>, options: PostgresConnectorOptions = {}): InfraConnector<SQL> {
|
|
@@ -78,7 +83,7 @@ export function postgres<T extends PostgresConfigShape>(config: AppConfig<T>, op
|
|
|
78
83
|
throw new InfraError("postgres shutdownTimeoutMs must be an integer 0..2147483647");
|
|
79
84
|
}
|
|
80
85
|
return {
|
|
81
|
-
token: options.token ??
|
|
86
|
+
token: options.token ?? IPostgres,
|
|
82
87
|
config,
|
|
83
88
|
create(configs) {
|
|
84
89
|
return new SQL(postgresConnectionOptions(config, configs));
|
|
@@ -11,9 +11,17 @@ import type { AppConfig, Secret } from "../../kernel";
|
|
|
11
11
|
import { RedisDistributedCacheBackend, type RedisDistributedCacheTuning } from "../cache/RedisDistributedCacheBackend";
|
|
12
12
|
import { reader, requireValue } from "../connectorConfig";
|
|
13
13
|
import { errorMessage, InfraError, type InfraConnector } from "../InfraConnector";
|
|
14
|
+
import { redactSensitiveText } from "../../../library/redaction";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const DEFAULT_REDIS_CONNECT_TIMEOUT_MS = 5_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Redis/Valkey client (native Bun `RedisClient`, no external dependencies) of
|
|
20
|
+
* the `redisConnect()` connector, injected by type:
|
|
21
|
+
* `constructor(private readonly redis: IRedis)`.
|
|
22
|
+
*/
|
|
23
|
+
export interface IRedis extends RedisClient {}
|
|
24
|
+
export const IRedis = createToken<IRedis>("IRedis") as InjectionToken<RedisClient>;
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Config interface required by the Redis connector. The subsystem config
|
|
@@ -30,6 +38,12 @@ export type RedisConnectorCacheMode =
|
|
|
30
38
|
|
|
31
39
|
export interface RedisConnectorOptions {
|
|
32
40
|
readonly token?: InjectionToken<RedisClient>;
|
|
41
|
+
/**
|
|
42
|
+
* How long the start waits for the first connection, ms (default 5000).
|
|
43
|
+
* The Bun client keeps retrying a refused connection; without a bound an
|
|
44
|
+
* unreachable Redis held the whole start until its timeout.
|
|
45
|
+
*/
|
|
46
|
+
readonly connectTimeoutMs?: number;
|
|
33
47
|
/**
|
|
34
48
|
* Publishes a distributed cache backend on top of the client: `{ cache: "distributed" }`.
|
|
35
49
|
* For fine tuning use `{ cache: { mode: "distributed", keyPrefix, ... } }`.
|
|
@@ -81,7 +95,7 @@ function distributedCacheProviders(tuning: RedisDistributedCacheTuning, token: I
|
|
|
81
95
|
* export const redisConfig = defineConfig("redis", { default: { url: "redis://localhost:6379" } });
|
|
82
96
|
* @Infra({ cache: redisConnect(redisConfig, { cache: "distributed" }) })
|
|
83
97
|
* export class AppInfra {}
|
|
84
|
-
* //
|
|
98
|
+
* // by type: class SessionsService { constructor(private readonly redis: IRedis) {} }
|
|
85
99
|
* ```
|
|
86
100
|
*/
|
|
87
101
|
export function redisConnect<T extends RedisConfigShape>(
|
|
@@ -89,7 +103,12 @@ export function redisConnect<T extends RedisConfigShape>(
|
|
|
89
103
|
options: RedisConnectorOptions = {},
|
|
90
104
|
): InfraConnector<RedisClient> {
|
|
91
105
|
const cache = options.cache;
|
|
92
|
-
const token = options.token ??
|
|
106
|
+
const token = options.token ?? IRedis;
|
|
107
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_REDIS_CONNECT_TIMEOUT_MS;
|
|
108
|
+
if (!Number.isSafeInteger(connectTimeoutMs) || connectTimeoutMs <= 0) {
|
|
109
|
+
throw new InfraError("redisConnect connectTimeoutMs must be a positive integer.");
|
|
110
|
+
}
|
|
111
|
+
const urls = new WeakMap<RedisClient, string>();
|
|
93
112
|
const cacheProviders =
|
|
94
113
|
cache === undefined
|
|
95
114
|
? undefined
|
|
@@ -101,13 +120,28 @@ export function redisConnect<T extends RedisConfigShape>(
|
|
|
101
120
|
providers: cacheProviders,
|
|
102
121
|
exports: cacheProviders ? [DISTRIBUTED_CACHE_BACKEND, DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE, IDistributedCache] : undefined,
|
|
103
122
|
create(configs) {
|
|
104
|
-
|
|
123
|
+
const url = redisUrl(reader(config, configs).get("url"));
|
|
124
|
+
const client = new RedisClient(url);
|
|
125
|
+
urls.set(client, url);
|
|
126
|
+
return client;
|
|
105
127
|
},
|
|
106
128
|
async connect(client) {
|
|
129
|
+
// The address without a password: it names the server in the error.
|
|
130
|
+
const target = redactSensitiveText(urls.get(client) ?? "redis");
|
|
131
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
132
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
133
|
+
timer = setTimeout(() => reject(new InfraError(`Infra connector "redis": failed to connect to ${target} within ${connectTimeoutMs} ms.`)), connectTimeoutMs);
|
|
134
|
+
});
|
|
107
135
|
try {
|
|
108
|
-
await client.connect();
|
|
136
|
+
await Promise.race([client.connect(), deadline]);
|
|
109
137
|
} catch (error) {
|
|
110
|
-
|
|
138
|
+
if (error instanceof InfraError) {
|
|
139
|
+
client.close();
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
throw new InfraError(`Infra connector "redis": failed to connect to ${target} — ${errorMessage(error)}`);
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timer);
|
|
111
145
|
}
|
|
112
146
|
},
|
|
113
147
|
dispose(client) {
|
package/core/infra/index.ts
CHANGED
|
@@ -10,10 +10,10 @@ export { reader, requireValue, type ConfigReader } from "./connectorConfig";
|
|
|
10
10
|
export { codexAppServerConnect, CODEX_APP_SERVER } from "./connectors/codex";
|
|
11
11
|
export { CodexError, RU_CODEX_MESSAGES, type CodexErrorCode, CODEX_MODEL_ID, CODEX_REASONING_EFFORT, type CodexClient, type CodexConfigShape, type CodexLogin, type CodexLoginMethod, type CodexModel, type CodexStatus, type CodexRunInput, type CodexChatMessage, type CodexTool, type CodexToolCall, type CodexToolResult } from "./connectors/codex/contracts";
|
|
12
12
|
|
|
13
|
-
export { postgres,
|
|
13
|
+
export { postgres, IPostgres, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
|
|
14
14
|
export {
|
|
15
15
|
redisConnect,
|
|
16
|
-
|
|
16
|
+
IRedis,
|
|
17
17
|
type RedisConfigShape,
|
|
18
18
|
type RedisConnectorCacheMode,
|
|
19
19
|
type RedisConnectorOptions,
|
|
@@ -26,7 +26,7 @@ export {
|
|
|
26
26
|
} from "./cache";
|
|
27
27
|
export {
|
|
28
28
|
openSearchConnect,
|
|
29
|
-
|
|
29
|
+
IOpenSearch,
|
|
30
30
|
OpenSearchClient,
|
|
31
31
|
type OpenSearchClientOptions,
|
|
32
32
|
type OpenSearchConfigShape,
|
package/core/kernel/Kernel.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class Kernel {
|
|
|
95
95
|
let startupDisposal: Promise<void> | undefined;
|
|
96
96
|
if (this.options.startupTimeoutMs > 0) {
|
|
97
97
|
startupTimer = setTimeout(
|
|
98
|
-
() => abortController.abort(new StartupTimeoutError(this.options.startupTimeoutMs)),
|
|
98
|
+
() => abortController.abort(new StartupTimeoutError(this.options.startupTimeoutMs, this.coordinator.startingServiceName)),
|
|
99
99
|
this.options.startupTimeoutMs,
|
|
100
100
|
);
|
|
101
101
|
}
|
|
@@ -21,6 +21,13 @@ interface LifecycleResolver {
|
|
|
21
21
|
* the {@link Kernel}, which bounds the whole graceful-stop sequence.
|
|
22
22
|
*/
|
|
23
23
|
export class LifecycleCoordinator {
|
|
24
|
+
private startingName?: string;
|
|
25
|
+
|
|
26
|
+
/** The hosted service whose `start` is running now, for the startup timeout. */
|
|
27
|
+
public get startingServiceName(): string | undefined {
|
|
28
|
+
return this.startingName;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
private hooks: readonly LifecycleHook[] = [];
|
|
25
32
|
/** Hosted services in actual start order (stop iterates in reverse). */
|
|
26
33
|
private readonly started: HostedService[] = [];
|
|
@@ -81,6 +88,7 @@ export class LifecycleCoordinator {
|
|
|
81
88
|
const group = phases[phaseIndex] as HostedService[];
|
|
82
89
|
for (let index = 0; index < group.length; index += 1) {
|
|
83
90
|
const service = group[index] as HostedService;
|
|
91
|
+
this.startingName = service.diagnosticName ?? service.constructor?.name;
|
|
84
92
|
const starting = Promise.resolve().then(() => {
|
|
85
93
|
signal?.throwIfAborted();
|
|
86
94
|
return service.start(signal);
|
|
@@ -97,6 +105,7 @@ export class LifecycleCoordinator {
|
|
|
97
105
|
throw error;
|
|
98
106
|
}
|
|
99
107
|
this.started.push(service);
|
|
108
|
+
this.startingName = undefined;
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
this.hostedCount = this.started.length;
|
|
@@ -47,6 +47,11 @@ export class SupervisedHostedService implements HostedService {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
public get diagnosticName(): string | undefined {
|
|
51
|
+
const service = this.inner ?? this.prepared;
|
|
52
|
+
return service === undefined ? undefined : service.diagnosticName ?? service.constructor.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
/** Passed on to every attempt's service before it starts. */
|
|
51
56
|
public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
|
|
52
57
|
this.diagnostics = diagnostics;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The start ran out of time. Not a configuration error: something it waited
|
|
3
|
+
* for did not answer, and `pending` names it when a hosted service was starting.
|
|
4
|
+
*/
|
|
5
|
+
export class StartupTimeoutError extends Error {
|
|
6
|
+
public constructor(public readonly timeoutMs: number, public readonly pending?: string) {
|
|
7
|
+
super(`Application startup did not finish within ${timeoutMs}ms${pending === undefined ? "." : `; still starting: ${pending}.`}`);
|
|
8
|
+
this.name = "StartupTimeoutError";
|
|
6
9
|
}
|
|
7
10
|
}
|
package/index.ts
CHANGED
package/package.json
CHANGED