@cat-factory/orchestration 0.283.2 → 0.284.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.
Files changed (39) hide show
  1. package/dist/container/dependencies.d.ts +16 -1
  2. package/dist/container/dependencies.d.ts.map +1 -1
  3. package/dist/container/runtime.d.ts +1 -0
  4. package/dist/container/runtime.d.ts.map +1 -1
  5. package/dist/container/runtime.js +5 -1
  6. package/dist/container/runtime.js.map +1 -1
  7. package/dist/container/use-case-service.d.ts +20 -0
  8. package/dist/container/use-case-service.d.ts.map +1 -0
  9. package/dist/container/use-case-service.js +62 -0
  10. package/dist/container/use-case-service.js.map +1 -0
  11. package/dist/container.d.ts +16 -1
  12. package/dist/container.d.ts.map +1 -1
  13. package/dist/container.js +11 -1
  14. package/dist/container.js.map +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/modules/useCases/InlineUseCaseService.d.ts +79 -0
  20. package/dist/modules/useCases/InlineUseCaseService.d.ts.map +1 -0
  21. package/dist/modules/useCases/InlineUseCaseService.js +220 -0
  22. package/dist/modules/useCases/InlineUseCaseService.js.map +1 -0
  23. package/dist/modules/useCases/LlmInlineUseCaseGenerator.d.ts +39 -0
  24. package/dist/modules/useCases/LlmInlineUseCaseGenerator.d.ts.map +1 -0
  25. package/dist/modules/useCases/LlmInlineUseCaseGenerator.js +224 -0
  26. package/dist/modules/useCases/LlmInlineUseCaseGenerator.js.map +1 -0
  27. package/dist/modules/useCases/useCaseUsage.d.ts +16 -0
  28. package/dist/modules/useCases/useCaseUsage.d.ts.map +1 -0
  29. package/dist/modules/useCases/useCaseUsage.js +40 -0
  30. package/dist/modules/useCases/useCaseUsage.js.map +1 -0
  31. package/dist/validation/validateInlineUseCases.d.ts +30 -0
  32. package/dist/validation/validateInlineUseCases.d.ts.map +1 -0
  33. package/dist/validation/validateInlineUseCases.js +114 -0
  34. package/dist/validation/validateInlineUseCases.js.map +1 -0
  35. package/dist/validation/validateRegistrations.d.ts +9 -1
  36. package/dist/validation/validateRegistrations.d.ts.map +1 -1
  37. package/dist/validation/validateRegistrations.js +23 -0
  38. package/dist/validation/validateRegistrations.js.map +1 -1
  39. package/package.json +11 -11
@@ -0,0 +1,220 @@
1
+ import { sanitizeDescriptorFields, validateDescriptorFields, withDescriptorFieldDefaults, } from '@cat-factory/contracts';
2
+ import { composeUseCasePrompt, describeError, NotFoundError, RateLimitedError, resolveUseCaseModelOption, UnavailableError, useCaseGenerationLimits, ValidationError, } from '@cat-factory/kernel';
3
+ /** The parameters a use case declares, as a list (absent ⇒ none). */
4
+ function parametersOf(useCase) {
5
+ return useCase.parameters ?? [];
6
+ }
7
+ export class InlineUseCaseService {
8
+ deps;
9
+ constructor(deps) {
10
+ this.deps = deps;
11
+ }
12
+ /** Every registered use case, projected for this scope (model availability included). */
13
+ async list(scope) {
14
+ const session = await this.discoverySession(scope);
15
+ return this.deps.registry.all().map((useCase) => this.project(useCase, session));
16
+ }
17
+ /** One registered use case by id, projected for this scope. */
18
+ async get(scope, useCaseId) {
19
+ const useCase = this.require(useCaseId);
20
+ return this.project(useCase, await this.discoverySession(scope));
21
+ }
22
+ /**
23
+ * Run one use case and answer with the generated text.
24
+ *
25
+ * The refusal ORDER is the point, and it is cheapest-first for the same reason the bug hunt's is:
26
+ * every step past the last one costs more than the step before it, so a request that was never
27
+ * going to run spends nothing. The registration lookup and the parameter check read nothing at
28
+ * all; binding the session reads the credential pool; the budget probe reads the spend ledger;
29
+ * only then does a vendor see a token.
30
+ */
31
+ async invoke(input) {
32
+ const useCase = this.require(input.useCaseId);
33
+ const generator = this.requireGenerator();
34
+ const parameters = this.validateParameters(useCase, input.parameters ?? {});
35
+ const option = this.requireModelOption(useCase, input.model);
36
+ const limits = useCaseGenerationLimits(useCase);
37
+ const temperature = requireWithinBounds(input.temperature, limits.temperature, 'temperature');
38
+ const maxOutputTokens = requireWithinBounds(input.maxOutputTokens, limits.maxOutputTokens, 'maxOutputTokens');
39
+ // NOT caught here, unlike the discovery path: a credential pool that could not be read is not
40
+ // an availability answer, and reporting it as "this model is unavailable" would send the caller
41
+ // to pick another one for a fault that has nothing to do with the model they named.
42
+ const session = await generator.forScope(input.scope);
43
+ const availability = session.availability(option);
44
+ if (!availability.available) {
45
+ throw new UnavailableError(`The model '${option.label}' cannot be served by this deployment`, 'use_case_model_unavailable', { model: option.id, cause: availability.reason });
46
+ }
47
+ if (await this.deps.isOverBudget?.(input.scope)) {
48
+ // Its OWN refusal rather than a generic failure: an exhausted budget is not a broken model,
49
+ // and the fix (raise the budget, or wait for the window to roll) is not the fix for a
50
+ // misconfigured provider. Fail-CLOSED, so no vendor call is made.
51
+ throw new RateLimitedError('This workspace has spent its configured model budget', 'budget_exhausted');
52
+ }
53
+ const prompt = composeUseCasePrompt(useCase, {
54
+ useCaseId: useCase.useCaseId,
55
+ workspaceId: input.scope.workspaceId,
56
+ parameters,
57
+ fields: parametersOf(useCase),
58
+ });
59
+ const generation = await session.generate({
60
+ useCaseId: useCase.useCaseId,
61
+ option,
62
+ system: prompt.system,
63
+ prompt: prompt.prompt,
64
+ temperature,
65
+ maxOutputTokens,
66
+ });
67
+ if (generation.text.trim() === '') {
68
+ // An empty visible reply means the model answered only into its private reasoning channel
69
+ // (seen on some reasoning models) or refused without saying so. Either way there is nothing
70
+ // to return, and a 200 carrying an empty string would read to a content editor as a model
71
+ // that had nothing to say about the scene.
72
+ this.deps.logger?.warn('An inline use case produced no text', {
73
+ workspaceId: input.scope.workspaceId,
74
+ useCaseId: useCase.useCaseId,
75
+ model: option.id,
76
+ finishReason: generation.finishReason,
77
+ });
78
+ throw new UnavailableError(`The model '${option.label}' returned no usable text`, 'use_case_empty_reply', { model: option.id, finishReason: generation.finishReason });
79
+ }
80
+ return {
81
+ useCaseId: useCase.useCaseId,
82
+ model: {
83
+ id: option.id,
84
+ label: option.label,
85
+ provider: generation.ref.provider,
86
+ model: generation.ref.model,
87
+ },
88
+ text: generation.text,
89
+ finishReason: generation.finishReason,
90
+ truncated: generation.finishReason === 'length',
91
+ usage: generation.usage,
92
+ };
93
+ }
94
+ /** The registered use case, or a 404 naming what was asked for. */
95
+ require(useCaseId) {
96
+ const useCase = this.deps.registry.get(useCaseId);
97
+ if (!useCase)
98
+ throw new NotFoundError('Use case', useCaseId, { reason: 'use_case_not_found' });
99
+ return useCase;
100
+ }
101
+ /**
102
+ * The generator, or a 503 naming the deployment-level gap.
103
+ *
104
+ * Distinct from a model being unavailable, which is what the per-option availability answers: an
105
+ * unconfigured deployment is fixed by wiring a model provider, and reporting every declared model
106
+ * as individually unavailable would send an operator looking for four missing keys instead.
107
+ */
108
+ requireGenerator() {
109
+ const generator = this.deps.generator;
110
+ if (!generator?.enabled) {
111
+ throw new UnavailableError('No model provider is configured, so use cases cannot be invoked', 'use_case_models_unconfigured');
112
+ }
113
+ return generator;
114
+ }
115
+ /**
116
+ * The session a DISCOVERY read projects against, or `undefined` when there is none to be had.
117
+ *
118
+ * Two causes land on the same undefined, and each is stated where it happens rather than
119
+ * inferred here: no provider is wired at all (ordinary, and the catalog says so per option), or
120
+ * the credential pool could not be read (a real fault, logged with its cause). Discovery answers
121
+ * either way, because a read that 500s tells a wrapper the surface does not exist when what
122
+ * failed was one query behind it.
123
+ */
124
+ async discoverySession(scope) {
125
+ const generator = this.deps.generator;
126
+ if (!generator?.enabled)
127
+ return undefined;
128
+ try {
129
+ return await generator.forScope(scope);
130
+ }
131
+ catch (error) {
132
+ this.deps.logger?.warn('Use-case model availability could not be resolved', {
133
+ workspaceId: scope.workspaceId,
134
+ ...describeError(error),
135
+ });
136
+ return undefined;
137
+ }
138
+ }
139
+ /**
140
+ * The caller's bag, checked against the declared parameters and frozen.
141
+ *
142
+ * The SHARED descriptor validator, so this surface refuses exactly what a reusable operation's
143
+ * create door refuses: unknown keys, wrong value types, missing required visible fields, values
144
+ * outside a `select`'s options. Every problem is reported at once, because a caller filling a
145
+ * form wants the whole list rather than one field per round trip.
146
+ */
147
+ validateParameters(useCase, supplied) {
148
+ const fields = parametersOf(useCase);
149
+ const withDefaults = withDescriptorFieldDefaults(fields, supplied);
150
+ const problems = validateDescriptorFields(fields, withDefaults);
151
+ if (problems.length > 0) {
152
+ throw new ValidationError(problems.join('; '), {
153
+ reason: 'use_case_parameters_invalid',
154
+ problems,
155
+ });
156
+ }
157
+ return sanitizeDescriptorFields(fields, withDefaults);
158
+ }
159
+ /** The option the invocation runs on, or a 422 naming what this use case does carry. */
160
+ requireModelOption(useCase, requested) {
161
+ const option = resolveUseCaseModelOption(useCase, requested);
162
+ if (!option) {
163
+ throw new ValidationError(`The use case '${useCase.useCaseId}' does not offer a model '${requested ?? ''}'`, {
164
+ reason: 'use_case_model_not_allowed',
165
+ // The allowed ids, so a caller holding a stale catalog can correct itself from the
166
+ // refusal rather than re-reading discovery to find out what changed.
167
+ allowed: useCase.models.map((declared) => declared.id),
168
+ });
169
+ }
170
+ return option;
171
+ }
172
+ /** One use case's wire projection, with each model's availability read off the bound session. */
173
+ project(useCase, session) {
174
+ return {
175
+ useCaseId: useCase.useCaseId,
176
+ label: useCase.label,
177
+ description: useCase.description,
178
+ ...(useCase.category ? { category: useCase.category } : {}),
179
+ models: useCase.models.map((option) => projectModel(useCase, option, session)),
180
+ parameters: [...parametersOf(useCase)],
181
+ generation: useCaseGenerationLimits(useCase),
182
+ };
183
+ }
184
+ }
185
+ /** One model option's wire projection. */
186
+ function projectModel(useCase, option, session) {
187
+ // With no session there is no provider to ask, so every option is unavailable for the same
188
+ // deployment-level cause. Reported per option rather than omitted: a wrapper rendering the
189
+ // picker still shows what this use case OFFERS, greyed out, instead of an empty list that reads
190
+ // like a use case with no models declared.
191
+ const availability = session
192
+ ? session.availability(option)
193
+ : { available: false, reason: 'provider_unavailable' };
194
+ return {
195
+ id: option.id,
196
+ label: option.label,
197
+ ...(option.description ? { description: option.description } : {}),
198
+ default: resolveUseCaseModelOption(useCase, undefined)?.id === option.id,
199
+ available: availability.available,
200
+ ...(availability.available ? {} : { unavailableReason: availability.reason }),
201
+ };
202
+ }
203
+ /**
204
+ * A knob the caller named, checked against the declared bounds, else the declared default.
205
+ *
206
+ * REFUSED rather than clamped, which is why the name says `require` and not `clamp`. A caller
207
+ * asking for temperature 2.5 against a ceiling of 1.2 is asking for output the deployment has
208
+ * decided not to produce, and silently running at 1.2 answers that request with a different
209
+ * generation while reporting success: the caller stores the text believing it came from the
210
+ * settings it asked for.
211
+ */
212
+ function requireWithinBounds(value, limit, name) {
213
+ if (value === undefined)
214
+ return limit.default;
215
+ if (!Number.isFinite(value) || value < limit.min || value > limit.max) {
216
+ throw new ValidationError(`'${name}' must be between ${limit.min} and ${limit.max} for this use case`, { reason: 'use_case_generation_out_of_range', field: name, min: limit.min, max: limit.max });
217
+ }
218
+ return value;
219
+ }
220
+ //# sourceMappingURL=InlineUseCaseService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InlineUseCaseService.js","sourceRoot":"","sources":["../../../src/modules/useCases/InlineUseCaseService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { InlineUseCaseGenerator, InlineUseCaseScope, InlineUseCaseSession, Logger, ModelFlavor, ModelProvider, ModelProviderResolver, ModelRef } from '@cat-factory/kernel';
2
+ /**
3
+ * How long one invocation may wait on the vendor, by default.
4
+ *
5
+ * Generous, because the ceiling on `maxOutputTokens` is 32,000 and a long scene legitimately takes
6
+ * minutes, but FINITE: the alternative is a request held open for as long as the transport allows,
7
+ * on a surface whose whole shape is "ask and be answered". A deployment narrows it per facade.
8
+ */
9
+ export declare const DEFAULT_USE_CASE_TIMEOUT_MS = 120000;
10
+ /** What the generator needs to resolve its models and reach the provider. */
11
+ export interface LlmInlineUseCaseGeneratorDeps {
12
+ /** Resolve a ModelProvider for a workspace's credential scope (preferred). */
13
+ modelProviderResolver?: ModelProviderResolver;
14
+ /** Static provider (e.g. a fake in tests) used when no resolver is set. */
15
+ modelProvider?: ModelProvider;
16
+ /** Resolve a model catalog id to a ref, under the deployment's route order. */
17
+ resolveBlockModel?: (modelId: string | undefined, providerPreference?: readonly ModelFlavor[]) => ModelRef | undefined;
18
+ /** Keep an ambient-eligible harness ref inline (local mode) instead of refusing it. */
19
+ runsInline?: (ref: ModelRef) => boolean;
20
+ /** The per-invocation deadline; absent ⇒ {@link DEFAULT_USE_CASE_TIMEOUT_MS}. */
21
+ timeoutMs?: number;
22
+ /** Facade logger; a failed generation with no trace is an unowned bug. */
23
+ logger?: Logger;
24
+ }
25
+ export declare class LlmInlineUseCaseGenerator implements InlineUseCaseGenerator {
26
+ private readonly deps;
27
+ constructor(deps: LlmInlineUseCaseGeneratorDeps);
28
+ /** Whether a generation can run at all (some provider is wired). */
29
+ get enabled(): boolean;
30
+ /**
31
+ * Resolve this request's credential pool ONCE and hand back the session that reads it.
32
+ *
33
+ * Not caught here: a pool that could not be read is a fact about the deployment, and each caller
34
+ * answers it differently (discovery says so per option and still publishes the catalog; an
35
+ * invocation lets it propagate). Swallowing it here would make both of them guess.
36
+ */
37
+ forScope(scope: InlineUseCaseScope): Promise<InlineUseCaseSession>;
38
+ }
39
+ //# sourceMappingURL=LlmInlineUseCaseGenerator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LlmInlineUseCaseGenerator.d.ts","sourceRoot":"","sources":["../../../src/modules/useCases/LlmInlineUseCaseGenerator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,sBAAsB,EAGtB,kBAAkB,EAClB,oBAAoB,EACpB,MAAM,EACN,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,QAAQ,EAET,MAAM,qBAAqB,CAAA;AAqC5B;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,SAAU,CAAA;AAWlD,6EAA6E;AAC7E,MAAM,WAAW,6BAA6B;IAC5C,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,qBAAqB,CAAA;IAC7C,2EAA2E;IAC3E,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,kBAAkB,CAAC,EAAE,SAAS,WAAW,EAAE,KACxC,QAAQ,GAAG,SAAS,CAAA;IACzB,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAA;IACvC,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AA+KD,qBAAa,yBAA0B,YAAW,sBAAsB;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,6BAA6B,EAAI;IAEpE,oEAAoE;IACpE,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;;OAMG;IACG,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAGvE;CACF"}
@@ -0,0 +1,224 @@
1
+ import { generateText } from 'ai';
2
+ import { describeError, getErrorMessage, resolveScopedModelProvider, runsOnSubscriptionHarness, UnavailableError, } from '@cat-factory/kernel';
3
+ import { catFactoryObservability } from '@cat-factory/agents';
4
+ import { readUseCaseUsage } from './useCaseUsage.js';
5
+ // ---------------------------------------------------------------------------
6
+ // The default {@link InlineUseCaseGenerator}: the inline LLM call behind an invocation of a
7
+ // registered use case.
8
+ //
9
+ // Structurally the `BugHuntAssessorService` twin, and for the same reason: an un-run-scoped inline
10
+ // call built from the model dependencies every facade already wires, so the feature needs no
11
+ // per-facade wiring of its own and the conformance harness swaps in a deterministic fake through
12
+ // the `InlineUseCaseGenerator` seam.
13
+ //
14
+ // Three things differ from every other inline caller. Two follow from the use case being a DECLARED
15
+ // narrowing rather than an engine step:
16
+ //
17
+ // - There is no block, no task and no preset consult. A use case names its own models, so nothing
18
+ // a workspace pinned elsewhere may reach in and change which model runs; the workspace's per-kind
19
+ // preset default would be exactly such a substitution.
20
+ // - A subscription-harness ref is REFUSED rather than degraded. Every other inline site degrades
21
+ // one to the routing default (`inlineModelRef`), which is right where the model is an
22
+ // implementation detail of a step. Here the model IS the request.
23
+ //
24
+ // The third follows from the surface being SYNCHRONOUS: the call is bounded by a deadline and one
25
+ // retry. Every other long-running model path in this repo is a dispatched job with a poll and a
26
+ // watchdog; this one is a request the caller is holding open, so an unbounded vendor stall would be
27
+ // paid for by whoever asked.
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * How long one invocation may wait on the vendor, by default.
31
+ *
32
+ * Generous, because the ceiling on `maxOutputTokens` is 32,000 and a long scene legitimately takes
33
+ * minutes, but FINITE: the alternative is a request held open for as long as the transport allows,
34
+ * on a surface whose whole shape is "ask and be answered". A deployment narrows it per facade.
35
+ */
36
+ export const DEFAULT_USE_CASE_TIMEOUT_MS = 120_000;
37
+ /**
38
+ * Retries the AI SDK may make inside one invocation.
39
+ *
40
+ * ONE, against the SDK's default of two: a transient 429 or 502 is worth a second attempt, and the
41
+ * third would be spent inside a deadline the caller is already waiting out. The deadline covers all
42
+ * attempts together (one signal, one absolute expiry), so retrying cannot extend it.
43
+ */
44
+ const USE_CASE_MAX_RETRIES = 1;
45
+ /**
46
+ * The AI SDK's finish reasons, mapped to the bounded wire class.
47
+ *
48
+ * `tool-calls` cannot occur (no tools are passed) and the SDK's `unknown`/`error` both mean the
49
+ * provider said nothing usable about why it stopped, so they land on `other` rather than on a
50
+ * fabricated `stop`. Reporting `stop` for an unknown finish would tell a caller the text is
51
+ * complete when nothing said so.
52
+ */
53
+ function mapFinishReason(reason) {
54
+ switch (reason) {
55
+ case 'stop':
56
+ return 'stop';
57
+ case 'length':
58
+ return 'length';
59
+ case 'content-filter':
60
+ return 'content-filter';
61
+ default:
62
+ return 'other';
63
+ }
64
+ }
65
+ /**
66
+ * The credential scope, as the model-provider resolver takes it.
67
+ *
68
+ * An absent account or user is OMITTED rather than passed as null, because the two are different
69
+ * instructions to the pool: omitted means "resolve the workspace's owning account yourself", while
70
+ * an explicit null means "this pool has no account tier". Passing null for an unknown account would
71
+ * silently drop every account-scoped key.
72
+ */
73
+ function modelScopeFor(scope) {
74
+ return {
75
+ workspaceId: scope.workspaceId,
76
+ ...(scope.accountId ? { accountId: scope.accountId } : {}),
77
+ ...(scope.userId ? { userId: scope.userId } : {}),
78
+ };
79
+ }
80
+ /**
81
+ * One request's generator: the credential pool, resolved once, plus the two answers that read it.
82
+ *
83
+ * Everything expensive happened in {@link LlmInlineUseCaseGenerator.forScope}, which is what makes
84
+ * `availability` synchronous. A discovery read over a catalog therefore probes every declared
85
+ * option for free, where a per-option resolution meant an `accountOf` read, a configured-providers
86
+ * read and a key LEASE (an atomic select-and-mark write plus a decrypt) each time.
87
+ */
88
+ class ScopedInlineUseCaseSession {
89
+ deps;
90
+ scope;
91
+ provider;
92
+ constructor(deps, scope, provider) {
93
+ this.deps = deps;
94
+ this.scope = scope;
95
+ this.provider = provider;
96
+ }
97
+ availability(option) {
98
+ const ref = this.refFor(option);
99
+ if (!ref)
100
+ return { available: false, reason: 'provider_unavailable' };
101
+ if (runsOnSubscriptionHarness(ref) && !this.deps.runsInline?.(ref)) {
102
+ // A subscription harness runs inside a per-run container against a pooled OAuth token. A use
103
+ // case has no container and no run, so this is a permanent property of the pairing rather
104
+ // than a missing credential, and it is reported as its own cause. The test goes through
105
+ // kernel's `runsOnSubscriptionHarness` rather than a local spelling of it, because a ref
106
+ // carrying `harness: 'pi'` is the case a bare truthiness test gets wrong.
107
+ return { available: false, reason: 'container_only' };
108
+ }
109
+ if (!this.provider)
110
+ return { available: false, reason: 'provider_unavailable' };
111
+ try {
112
+ this.provider.resolve(ref);
113
+ }
114
+ catch (error) {
115
+ // `resolve` throws for a provider id with no registered resolver, which on this platform means
116
+ // the deployment configured no credentials for it. Logged rather than swallowed: an operator
117
+ // reading "unavailable" on a model they believe they configured needs the resolver's own
118
+ // message, which names what IS registered.
119
+ this.deps.logger?.debug('A use-case model did not resolve', {
120
+ workspaceId: this.scope.workspaceId,
121
+ model: option.id,
122
+ ...describeError(error),
123
+ });
124
+ return { available: false, reason: 'provider_unavailable' };
125
+ }
126
+ return { available: true, ref };
127
+ }
128
+ async generate(request) {
129
+ // Re-checked rather than trusted, and it costs nothing now that the pool is in hand: the port
130
+ // is called by the service AND directly by anything else holding a session, so `generate` owns
131
+ // the same refusal rather than assuming a caller made it.
132
+ const availability = this.availability(request.option);
133
+ if (!availability.available) {
134
+ throw new UnavailableError(`The model '${request.option.label}' cannot be served by this deployment`, 'use_case_model_unavailable', { model: request.option.id, cause: availability.reason });
135
+ }
136
+ const provider = this.provider;
137
+ if (!provider) {
138
+ throw new UnavailableError('No model provider is configured, so use cases cannot be invoked', 'use_case_models_unconfigured');
139
+ }
140
+ const timeoutMs = this.deps.timeoutMs ?? DEFAULT_USE_CASE_TIMEOUT_MS;
141
+ // ONE signal for the whole call, retries included, so the deadline is an absolute expiry rather
142
+ // than a per-attempt one. `aborted` is then what tells a timeout from a vendor error, without
143
+ // sniffing an error shape the SDK is free to change.
144
+ const deadline = AbortSignal.timeout(timeoutMs);
145
+ try {
146
+ const result = await generateText({
147
+ model: provider.resolve(availability.ref),
148
+ system: request.system,
149
+ prompt: request.prompt,
150
+ temperature: request.temperature,
151
+ maxOutputTokens: request.maxOutputTokens,
152
+ abortSignal: deadline,
153
+ maxRetries: USE_CASE_MAX_RETRIES,
154
+ // The call is tagged with the use case as its agent kind, so its tokens land in the same
155
+ // rollups every other inline call does and an operator can see what an editor is spending
156
+ // per use case rather than as one undifferentiated "inline" bucket.
157
+ providerOptions: catFactoryObservability({
158
+ agentKind: request.useCaseId,
159
+ workspaceId: this.scope.workspaceId,
160
+ }),
161
+ });
162
+ return {
163
+ text: result.text,
164
+ finishReason: mapFinishReason(result.finishReason),
165
+ usage: readUseCaseUsage(result.usage),
166
+ ref: availability.ref,
167
+ };
168
+ }
169
+ catch (error) {
170
+ throw this.failed(request, error, deadline.aborted ? timeoutMs : undefined);
171
+ }
172
+ }
173
+ /**
174
+ * The refusal for a call the vendor did not complete.
175
+ *
176
+ * A timeout is its OWN reason rather than one more generation failure, because the caller's move
177
+ * differs: a failed call is worth surfacing to whoever asked, a call that ran out of time is
178
+ * worth retrying with a smaller reply budget. Collapsing them would hide the only one of the two
179
+ * that the caller can do something about.
180
+ */
181
+ failed(request, error, timedOutAfterMs) {
182
+ const message = timedOutAfterMs === undefined
183
+ ? `The use-case model '${request.option.label}' failed: ${getErrorMessage(error)}`
184
+ : `The use-case model '${request.option.label}' did not answer within ${timedOutAfterMs}ms`;
185
+ this.deps.logger?.warn(message, {
186
+ workspaceId: this.scope.workspaceId,
187
+ useCaseId: request.useCaseId,
188
+ model: request.option.id,
189
+ ...describeError(error),
190
+ });
191
+ return new UnavailableError(message, timedOutAfterMs === undefined ? 'use_case_generation_failed' : 'use_case_generation_timeout', { model: request.option.id });
192
+ }
193
+ /** The ref one declared option resolves to, or undefined when this deployment cannot serve it. */
194
+ refFor(option) {
195
+ if (option.source.kind === 'provider')
196
+ return option.source.ref;
197
+ // A catalog id resolves under the DEPLOYMENT's default route order rather than a preset's: a
198
+ // use case is workspace-agnostic by construction (nothing about it is stored per workspace), so
199
+ // there is no preset in force to read an order off.
200
+ return this.deps.resolveBlockModel?.(option.source.modelId);
201
+ }
202
+ }
203
+ export class LlmInlineUseCaseGenerator {
204
+ deps;
205
+ constructor(deps) {
206
+ this.deps = deps;
207
+ }
208
+ /** Whether a generation can run at all (some provider is wired). */
209
+ get enabled() {
210
+ return !!this.deps.modelProviderResolver || !!this.deps.modelProvider;
211
+ }
212
+ /**
213
+ * Resolve this request's credential pool ONCE and hand back the session that reads it.
214
+ *
215
+ * Not caught here: a pool that could not be read is a fact about the deployment, and each caller
216
+ * answers it differently (discovery says so per option and still publishes the catalog; an
217
+ * invocation lets it propagate). Swallowing it here would make both of them guess.
218
+ */
219
+ async forScope(scope) {
220
+ const provider = await resolveScopedModelProvider(modelScopeFor(scope), this.deps);
221
+ return new ScopedInlineUseCaseSession(this.deps, scope, provider);
222
+ }
223
+ }
224
+ //# sourceMappingURL=LlmInlineUseCaseGenerator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LlmInlineUseCaseGenerator.js","sourceRoot":"","sources":["../../../src/modules/useCases/LlmInlineUseCaseGenerator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { UseCaseUsage } from '@cat-factory/contracts';
2
+ /**
3
+ * The invocation's usage, as the public surface publishes it.
4
+ *
5
+ * The billed input is taken from the vendor's RAW payload through the shared reconciler, and the
6
+ * SDK's own flat total is the floor rather than the alternative: whichever is larger is the one
7
+ * that cannot be an understatement, and on every provider mapping in this build the two agree
8
+ * (each already folds both cache classes into its total). The floor is what keeps a provider that
9
+ * passes no `raw` through, or one whose payload this build does not recognise, from publishing a 0.
10
+ *
11
+ * `totalTokens` is the SUM rather than any total the provider reported, so the three numbers a
12
+ * caller receives always add up. A published total that disagreed with its own two parts would
13
+ * leave a consumer no way to tell which of them to trust.
14
+ */
15
+ export declare function readUseCaseUsage(usage: unknown): UseCaseUsage;
16
+ //# sourceMappingURL=useCaseUsage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCaseUsage.d.ts","sourceRoot":"","sources":["../../../src/modules/useCases/useCaseUsage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAsB1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAS7D"}
@@ -0,0 +1,40 @@
1
+ import { readInputTokenClasses } from '@cat-factory/agents';
2
+ // ---------------------------------------------------------------------------
3
+ // What one inline use-case invocation cost, read off the AI SDK's usage object into the three
4
+ // numbers the public surface publishes.
5
+ //
6
+ // Its own module with its own test because the reading is NOT the obvious field read. The SDK's
7
+ // flat `inputTokens` is whatever the provider's own mapping put there, and vendors disagree about
8
+ // whether a cached prefix is INSIDE the prompt count (OpenAI: `prompt_tokens` covers it) or BESIDE
9
+ // it (Anthropic: `input_tokens` is fresh-only, with `cache_read_input_tokens` and
10
+ // `cache_creation_input_tokens` alongside). This repo already owns that reconciliation once, in
11
+ // `readInputTokenClasses`, and it is deliberately read here rather than re-spelled: a hand-rolled
12
+ // `usage.inputTokens ?? usage.promptTokens` understates a cache-heavy call on half the vendors,
13
+ // and the surface publishes that number as what was BILLED, which a wrapper metering its own users
14
+ // would then under-bill from silently.
15
+ // ---------------------------------------------------------------------------
16
+ /** A usable token count, else 0. Never coerced, never negative. */
17
+ function count(value) {
18
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
19
+ }
20
+ /**
21
+ * The invocation's usage, as the public surface publishes it.
22
+ *
23
+ * The billed input is taken from the vendor's RAW payload through the shared reconciler, and the
24
+ * SDK's own flat total is the floor rather than the alternative: whichever is larger is the one
25
+ * that cannot be an understatement, and on every provider mapping in this build the two agree
26
+ * (each already folds both cache classes into its total). The floor is what keeps a provider that
27
+ * passes no `raw` through, or one whose payload this build does not recognise, from publishing a 0.
28
+ *
29
+ * `totalTokens` is the SUM rather than any total the provider reported, so the three numbers a
30
+ * caller receives always add up. A published total that disagreed with its own two parts would
31
+ * leave a consumer no way to tell which of them to trust.
32
+ */
33
+ export function readUseCaseUsage(usage) {
34
+ const reported = (usage ?? {});
35
+ const classes = readInputTokenClasses(reported.raw);
36
+ const inputTokens = Math.max(classes.fresh + classes.cacheRead + classes.cacheWrite, count(reported.inputTokens));
37
+ const outputTokens = count(reported.outputTokens);
38
+ return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
39
+ }
40
+ //# sourceMappingURL=useCaseUsage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCaseUsage.js","sourceRoot":"","sources":["../../../src/modules/useCases/useCaseUsage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAG3D,8EAA8E;AAC9E,8FAA8F;AAC9F,wCAAwC;AACxC,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,mGAAmG;AACnG,kFAAkF;AAClF,gGAAgG;AAChG,kGAAkG;AAClG,gGAAgG;AAChG,mGAAmG;AACnG,uCAAuC;AACvC,8EAA8E;AAE9E,mEAAmE;AACnE,SAAS,KAAK,CAAC,KAAc;IAC3B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACrF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,MAAM,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAA;IACzD,MAAM,OAAO,GAAG,qBAAqB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACnD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,EACtD,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAC5B,CAAA;IACD,MAAM,YAAY,GAAG,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAA;IACjD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,GAAG,YAAY,EAAE,CAAA;AAC/E,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { InlineUseCaseDefinition } from '@cat-factory/kernel';
2
+ import type { RegistrationProblem } from './validateRegistrations.js';
3
+ /**
4
+ * Deployment-registered INLINE USE CASES: every way a registration is broken that nothing at run
5
+ * time can recover from, minus its parameter form (which its host still runs the SHARED descriptor
6
+ * checker over, alongside every other registered form).
7
+ *
8
+ * Its own module rather than more of `validateRegistrations.ts` for the reason the binary-generator
9
+ * section has one: a cohesive concern with a growing rule set, and a host at its size ratchet.
10
+ *
11
+ * All errors, by the bar the rest of that validator uses: each is fully knowable from the
12
+ * registration itself. Boot is also the only door these ever reach. A use case is code, so no write
13
+ * boundary refuses it, and every fault below is either silent or MISATTRIBUTED at request time:
14
+ *
15
+ * - a malformed id is unaddressable, because the id IS the path segment;
16
+ * - an empty or ambiguously-defaulted model list means an invocation naming no model either cannot
17
+ * resolve one or resolves whichever the author happened to list first, which is exactly the
18
+ * substitution the narrowing exists to prevent;
19
+ * - a bound whose default sits outside its own range refuses every invocation that omits the knob,
20
+ * naming a value the caller never sent;
21
+ * - a catalog model id nothing resolves publishes as `provider_unavailable`, whose documented
22
+ * remedy is "configure the provider": the operator then hunts a key for a model that will never
23
+ * resolve, and the two-member reason vocabulary the surface publishes cannot say otherwise;
24
+ * - a caption outside the PUBLISHED bounds serves a shape the surface's own OpenAPI calls
25
+ * impossible, and silently, because a response is not re-validated on the way out;
26
+ * - a blank `systemPrompt` is the one the type comment already argues about: a use case with no
27
+ * instruction is an unrestricted model call wearing a name.
28
+ */
29
+ export declare function inlineUseCaseProblems(useCase: InlineUseCaseDefinition): RegistrationProblem[];
30
+ //# sourceMappingURL=validateInlineUseCases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateInlineUseCases.d.ts","sourceRoot":"","sources":["../../src/validation/validateInlineUseCases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAKlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,uBAAuB,GAAG,mBAAmB,EAAE,CAgB7F"}