@gr8ful/spf 0.19.0 → 0.19.1
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 +8 -0
- package/assets/skill/references/config.md +1 -0
- package/dist/cli/commands/doctor.js +25 -3
- package/dist/cli/commands/estimate.d.ts +22 -6
- package/dist/cli/commands/estimate.js +32 -10
- package/dist/cli/commands/loop.d.ts +20 -0
- package/dist/cli/commands/loop.js +20 -1
- package/dist/cli/commands/ui.js +2 -1
- package/dist/cli/commands/watch.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/interview.js +13 -0
- package/dist/cli/ui/run_dashboard.js +13 -7
- package/dist/core/agent_cc.d.ts +19 -3
- package/dist/core/agent_cc.js +38 -18
- package/dist/core/agent_flue.js +51 -14
- package/dist/core/agent_opencode.d.ts +62 -25
- package/dist/core/agent_opencode.js +71 -30
- package/dist/core/agents.d.ts +51 -4
- package/dist/core/agents.js +79 -4
- package/dist/core/console.d.ts +24 -4
- package/dist/core/console.js +20 -7
- package/dist/core/data_types.d.ts +300 -19
- package/dist/core/data_types.js +134 -5
- package/dist/core/issues/jira_provider.d.ts +51 -1
- package/dist/core/issues/jira_provider.js +69 -1
- package/dist/core/issues/provider.d.ts +23 -0
- package/dist/core/loop.d.ts +39 -1
- package/dist/core/loop.js +33 -2
- package/dist/core/ollama_provider.d.ts +96 -13
- package/dist/core/ollama_provider.js +172 -26
- package/dist/core/otel.js +10 -1
- package/dist/core/otel_propagation.d.ts +168 -24
- package/dist/core/otel_propagation.js +219 -43
- package/dist/core/permissions.d.ts +16 -1
- package/dist/core/permissions.js +91 -3
- package/dist/core/providers.js +8 -3
- package/dist/core/refine.js +13 -1
- package/dist/core/runner.d.ts +33 -2
- package/dist/core/runner.js +40 -5
- package/dist/core/tiering.js +7 -3
- package/dist/core/tracer.d.ts +7 -1
- package/dist/core/tracer.js +15 -3
- package/dist/ui/server/db.d.ts +8 -1
- package/dist/ui/server/db.js +21 -4
- package/dist/ui/server/serve.d.ts +7 -0
- package/dist/ui/server/serve.js +10 -7
- package/dist/ui/shared/types.d.ts +16 -0
- package/package.json +1 -1
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
* `type` imports below are erased at compile time (verbatimModuleSyntax) and
|
|
42
42
|
* cost nothing at runtime either way.
|
|
43
43
|
*/
|
|
44
|
+
import { context as contextApi, isSpanContextValid, trace as traceApi } from "@opentelemetry/api";
|
|
45
|
+
import { isFluePropagationInstalled, X_CORRELATION_ID_HEADER, X_SPF_AGENT_HEADER } from "./otel_propagation.js";
|
|
46
|
+
import { newId } from "./utils.js";
|
|
44
47
|
// Ollama has no auth of its own — `pi-ai`'s auth resolution always calls
|
|
45
48
|
// `getClientApiKey()` before a dispatch, and that call throws "No API key
|
|
46
49
|
// for provider: ollama" if the resolved key is falsy (verified live: the
|
|
@@ -51,6 +54,65 @@
|
|
|
51
54
|
// sends anywhere Ollama would look at it: Ollama's OpenAI-compatible server
|
|
52
55
|
// does not check the Authorization header's contents.
|
|
53
56
|
const DUMMY_API_KEY = "ollama-local-unused";
|
|
57
|
+
/**
|
|
58
|
+
* `OLLAMA_API_KEY`, trimmed, when the operator has set one — e.g. the
|
|
59
|
+
* Briefs gateway (Envoy AI Gateway) enforces a per-client bearer and 401s
|
|
60
|
+
* the dummy key `DUMMY_API_KEY` was designed for a bare local server that
|
|
61
|
+
* checks nothing. Falls back to the dummy exactly as before when unset, so
|
|
62
|
+
* THIS function's own return value — the resolved `Authorization` bearer —
|
|
63
|
+
* is byte-identical to pre-gateway behavior for a bare local Ollama server
|
|
64
|
+
* (the common case this module was built for). MINOR-H: that is narrower
|
|
65
|
+
* than "the whole request is unchanged" — it is not, even with
|
|
66
|
+
* `OLLAMA_API_KEY` unset: a `traceparent` (and, once `agent_flue.ts` has an
|
|
67
|
+
* adw_id/agent_name to give it, `x-correlation-id`/`x-spf-agent`) is ALWAYS
|
|
68
|
+
* sent now, gateway or no gateway (see the "Gateway headers" section below).
|
|
69
|
+
* A bare local Ollama server ignores headers it doesn't recognize, so this
|
|
70
|
+
* is harmless — just not byte-identical. Read fresh inside `resolve()` (see
|
|
71
|
+
* its call site below) — never cached — so a key exported mid-process (or
|
|
72
|
+
* changed) takes effect on the very next dispatch with no re-registration.
|
|
73
|
+
*
|
|
74
|
+
* MINOR 3: exported so `doctor.ts`'s `OLLAMA_BASE_URL reachability` probe
|
|
75
|
+
* calls this SAME function rather than reading `process.env.OLLAMA_API_KEY`
|
|
76
|
+
* raw — a prior version of that probe sent NO `Authorization` header at all
|
|
77
|
+
* when the env var was unset, which diverges from what a real dispatch
|
|
78
|
+
* sends (the dummy bearer below, always). Against a gateway that rejects a
|
|
79
|
+
* request with no `Authorization` header at all differently than one with a
|
|
80
|
+
* wrong/dummy bearer, that divergence could make doctor report reachable
|
|
81
|
+
* when a real dispatch would 401, or vice versa. Calling `ollamaApiKey()` in
|
|
82
|
+
* both places means doctor's probe and a real dispatch send byte-identical
|
|
83
|
+
* bearers for the same env state.
|
|
84
|
+
*/
|
|
85
|
+
export function ollamaApiKey() {
|
|
86
|
+
const key = (process.env.OLLAMA_API_KEY ?? "").trim();
|
|
87
|
+
return key || DUMMY_API_KEY;
|
|
88
|
+
}
|
|
89
|
+
/** Must NEVER be sent — Envoy/Switchyard own it end-to-end; a client-supplied value breaks their sampling. Exported only so tests can assert its absence by name, not a literal string. The sole surviving export of this name in the codebase — see MINOR-G in this change's review; `otel_propagation.ts` no longer has one now that `XRequestIdPropagator` is gone (BLOCKER B). */
|
|
90
|
+
export const X_REQUEST_ID_HEADER = "x-request-id";
|
|
91
|
+
/**
|
|
92
|
+
* A fresh W3C `traceparent` for one outbound call — used only on the NOT
|
|
93
|
+
* INSTALLED path (see the section above); when propagation IS installed,
|
|
94
|
+
* `resolve()` does not call this at all, relying entirely on the
|
|
95
|
+
* instrumentation's own per-request injection instead (this is what fixed
|
|
96
|
+
* BLOCKER A/MAJOR-C: this function used to be called unconditionally, and
|
|
97
|
+
* on the installed path it silently reused the one still-open span's
|
|
98
|
+
* traceparent for every call inside that span, which is both a duplicate
|
|
99
|
+
* header AND not actually fresh per call).
|
|
100
|
+
*
|
|
101
|
+
* Reuses the active OTel span context when one is installed and current —
|
|
102
|
+
* the same trace this call's other telemetry already belongs to — falling
|
|
103
|
+
* back to a brand-new random trace/span id pair when there is none (no span
|
|
104
|
+
* active at this exact point, e.g. a stray call before any span opened), so
|
|
105
|
+
* the gateway still gets a well-formed, per-call-unique traceparent either
|
|
106
|
+
* way. Never throws; `isSpanContextValid` is the same guard
|
|
107
|
+
* `otel_propagation.ts`'s own propagators use.
|
|
108
|
+
*/
|
|
109
|
+
export function freshTraceparent() {
|
|
110
|
+
const active = traceApi.getSpanContext(contextApi.active());
|
|
111
|
+
if (active && isSpanContextValid(active)) {
|
|
112
|
+
return `00-${active.traceId}-${active.spanId}-01`;
|
|
113
|
+
}
|
|
114
|
+
return `00-${newId(32)}-${newId(16)}-01`;
|
|
115
|
+
}
|
|
54
116
|
// Advisory only: pi-ai's `openai-completions` api reads this per REQUEST via
|
|
55
117
|
// its own `options.maxTokens`, not from `Model.maxTokens` directly — the
|
|
56
118
|
// field here only feeds Flue's compaction-reserve sizing (moot in practice
|
|
@@ -71,14 +133,25 @@ const DEFAULT_MAX_TOKENS = 8192;
|
|
|
71
133
|
*
|
|
72
134
|
* `OLLAMA_BASE_URL` is read fresh (via `ollamaBaseUrl()`) at each
|
|
73
135
|
* registration call, and the whole union is re-registered at whatever URL
|
|
74
|
-
* is current AT THAT MOMENT
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* per-agent override, which isn't.
|
|
136
|
+
* is current AT THAT MOMENT. `registerOllamaModel` re-runs this rebuild on
|
|
137
|
+
* EVERY call now (see its own doc for why: MAJOR-D's gateway-header
|
|
138
|
+
* re-stamping needs it), so in practice a mid-process `OLLAMA_BASE_URL`
|
|
139
|
+
* change is picked up by the very next dispatch to ANY already-registered
|
|
140
|
+
* id, not just the next brand-new one.
|
|
80
141
|
*/
|
|
81
142
|
const registeredIds = new Set();
|
|
143
|
+
/**
|
|
144
|
+
* The `GatewayCallContext` each model id was MOST RECENTLY registered with
|
|
145
|
+
* — see the "Gateway headers" section above for why `x-correlation-id`/
|
|
146
|
+
* `x-spf-agent` are static per-model (when otel propagation isn't
|
|
147
|
+
* installed) rather than resolved per-call. Keyed by model id so a union
|
|
148
|
+
* re-registration (triggered by ANY registration call, new id or repeat —
|
|
149
|
+
* see `registerOllamaModel`'s MAJOR-D doc) can rebuild every
|
|
150
|
+
* already-registered id's `Model.headers` from the context it MOST
|
|
151
|
+
* RECENTLY got, rather than dropping it or freezing it at first
|
|
152
|
+
* registration.
|
|
153
|
+
*/
|
|
154
|
+
const registrationContext = new Map();
|
|
82
155
|
// Registrations currently in flight, keyed by model id — lets a second
|
|
83
156
|
// caller for the SAME id that arrives before the first `await` resolves
|
|
84
157
|
// join that in-progress registration instead of returning immediately with
|
|
@@ -96,7 +169,25 @@ export function ollamaBaseUrl() {
|
|
|
96
169
|
const raw = (process.env.OLLAMA_BASE_URL ?? "").trim();
|
|
97
170
|
return raw || "http://localhost:11434/v1";
|
|
98
171
|
}
|
|
99
|
-
function modelFor(id, baseUrl) {
|
|
172
|
+
function modelFor(id, baseUrl, ctx) {
|
|
173
|
+
// Static per-model headers — see the "Gateway headers" section above for
|
|
174
|
+
// why `x-correlation-id`/`x-spf-agent` live here rather than in
|
|
175
|
+
// `resolve()`, and ONLY on the NOT INSTALLED path: when
|
|
176
|
+
// `isFluePropagationInstalled()` is true, `GatewayHeadersPropagator`
|
|
177
|
+
// already injects both per real request, correctly attributed per
|
|
178
|
+
// session — stamping them here too would double them up on the wire
|
|
179
|
+
// (BLOCKER A's bug, for these two headers instead of `traceparent`).
|
|
180
|
+
// Omitted entirely (no `headers` key at all) when there is nothing to
|
|
181
|
+
// stamp — propagation installed, or `ctx` absent/empty — so a caller that
|
|
182
|
+
// never passes one, or a run with otel configured, gets a `Model` with no
|
|
183
|
+
// static headers.
|
|
184
|
+
const staticHeaders = {};
|
|
185
|
+
if (!isFluePropagationInstalled()) {
|
|
186
|
+
if (ctx?.adwId)
|
|
187
|
+
staticHeaders[X_CORRELATION_ID_HEADER] = ctx.adwId;
|
|
188
|
+
if (ctx?.agentName)
|
|
189
|
+
staticHeaders[X_SPF_AGENT_HEADER] = ctx.agentName;
|
|
190
|
+
}
|
|
100
191
|
return {
|
|
101
192
|
id,
|
|
102
193
|
name: id,
|
|
@@ -113,30 +204,55 @@ function modelFor(id, baseUrl) {
|
|
|
113
204
|
// silently truncating requests.
|
|
114
205
|
contextWindow: 0,
|
|
115
206
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
207
|
+
...(Object.keys(staticHeaders).length > 0 ? { headers: staticHeaders } : {}),
|
|
116
208
|
};
|
|
117
209
|
}
|
|
118
210
|
/**
|
|
119
211
|
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
120
212
|
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
121
|
-
* id ever registered this process.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
213
|
+
* id ever registered this process. NOT idempotent w.r.t. `ctx` (see MAJOR-D
|
|
214
|
+
* below) — every call re-runs the union re-registration (dynamic imports
|
|
215
|
+
* are cheap after the first, and `setProvider()` is a cheap in-memory
|
|
216
|
+
* upsert), so this id's `Model.headers` always reflect the MOST RECENT
|
|
217
|
+
* `ctx` this function was called with, not just the first. A concurrent
|
|
218
|
+
* call for the SAME id joins the in-flight registration rather than running
|
|
219
|
+
* a second one in parallel (see `inflight`'s doc); `registeredIds` itself
|
|
220
|
+
* is only ever updated AFTER `setProvider()` succeeds, so a failed attempt
|
|
221
|
+
* (a bad install, a bundler that can't resolve the deep `.lazy` subpath, a
|
|
222
|
+
* future validation error) leaves the id unregistered and eligible for a
|
|
223
|
+
* real retry — not permanently and misleadingly marked "done" while
|
|
224
|
+
* nothing is actually registered.
|
|
130
225
|
*
|
|
131
226
|
* Must complete before the FIRST Flue dispatch that names this model
|
|
132
227
|
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
133
|
-
* but is equally safe to call again later with a new id
|
|
134
|
-
*
|
|
135
|
-
* added without orphaning the first (see the `registeredIds` doc
|
|
228
|
+
* but is equally safe to call again later with a new id, or the SAME id
|
|
229
|
+
* again, mid-process — a new id's union re-registration is how a second
|
|
230
|
+
* model gets added without orphaning the first (see the `registeredIds` doc
|
|
231
|
+
* above); a repeat of the SAME id is how MAJOR-D below is fixed.
|
|
232
|
+
*
|
|
233
|
+
* MAJOR-D (fixed): `ctx`, when given AND `isFluePropagationInstalled()` is
|
|
234
|
+
* false (see the "Gateway headers" section above — when it's true, these
|
|
235
|
+
* two headers come from the per-request `GatewayHeadersPropagator`
|
|
236
|
+
* instead), is stamped onto this id's `Model.headers` as
|
|
237
|
+
* `x-correlation-id`/`x-spf-agent`. A PRIOR version of this function
|
|
238
|
+
* returned immediately for an already-registered id (a false comment
|
|
239
|
+
* claimed "one spf process runs one adw_id for its whole lifetime" to
|
|
240
|
+
* justify this) — which meant every later agent/adw_id sharing a model id
|
|
241
|
+
* within one process (spf `loop`/`fanout`/`watch`, which run many adw_ids
|
|
242
|
+
* in ONE process, `fanout` concurrently) silently kept the FIRST
|
|
243
|
+
* registration's headers forever. Re-running the full registration on every
|
|
244
|
+
* call, unconditionally, fixes that for every case except one, which
|
|
245
|
+
* remains and is not silently swallowed: two flue agents dispatching
|
|
246
|
+
* CONCURRENTLY (not sequentially) to the SAME `ollama/<id>` model id race on
|
|
247
|
+
* `registrationContext`/`setProvider()` — whichever registration's
|
|
248
|
+
* `setProvider()` call lands last wins the headers BOTH calls' subsequent
|
|
249
|
+
* dispatches see, until the next registration for that id. This is a
|
|
250
|
+
* `fanout` concurrency > 1 scenario specifically (two DIFFERENT agents,
|
|
251
|
+
* same process, same model id, truly overlapping registrations) — a
|
|
252
|
+
* sequential loop/watch never hits it, since each call's `await` completes
|
|
253
|
+
* before the next one starts.
|
|
136
254
|
*/
|
|
137
|
-
export async function registerOllamaModel(modelId) {
|
|
138
|
-
if (registeredIds.has(modelId))
|
|
139
|
-
return;
|
|
255
|
+
export async function registerOllamaModel(modelId, ctx) {
|
|
140
256
|
const existing = inflight.get(modelId);
|
|
141
257
|
if (existing)
|
|
142
258
|
return existing;
|
|
@@ -156,8 +272,9 @@ export async function registerOllamaModel(modelId) {
|
|
|
156
272
|
// AFTER `setProvider()` below succeeds (see this function's doc).
|
|
157
273
|
const ids = new Set(registeredIds);
|
|
158
274
|
ids.add(modelId);
|
|
275
|
+
registrationContext.set(modelId, ctx ?? {});
|
|
159
276
|
const baseUrl = ollamaBaseUrl();
|
|
160
|
-
const models = [...ids].map((id) => modelFor(id, baseUrl));
|
|
277
|
+
const models = [...ids].map((id) => modelFor(id, baseUrl, registrationContext.get(id)));
|
|
161
278
|
const options = {
|
|
162
279
|
id: "ollama",
|
|
163
280
|
name: "Ollama (local)",
|
|
@@ -165,9 +282,37 @@ export async function registerOllamaModel(modelId) {
|
|
|
165
282
|
auth: {
|
|
166
283
|
apiKey: {
|
|
167
284
|
name: "Ollama (keyless)",
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
285
|
+
// Fresh per real dispatch (pi-ai reinvokes `resolve()` on every
|
|
286
|
+
// `Models.stream()`/`applyAuth()` call, never caching it — see the
|
|
287
|
+
// "Gateway headers" section above) — `apiKey` honors a real
|
|
288
|
+
// `OLLAMA_API_KEY` when the operator set one (e.g. the Briefs
|
|
289
|
+
// gateway's per-client bearer), falling back to the DUMMY_API_KEY
|
|
290
|
+
// a bare keyless local server needs (see its own doc for why that
|
|
291
|
+
// can't just be "no key needed" instead).
|
|
292
|
+
//
|
|
293
|
+
// `headers.traceparent` is minted here ONLY when
|
|
294
|
+
// `isFluePropagationInstalled()` is false — checked fresh on every
|
|
295
|
+
// call, since propagation can be installed partway through this
|
|
296
|
+
// process's lifetime (the first ollama dispatch in a run with
|
|
297
|
+
// otel configured registers the model BEFORE
|
|
298
|
+
// `installFluePropagation()` runs — see `agent_flue.ts`'s `run()`
|
|
299
|
+
// — so a later dispatch on the SAME already-registered model must
|
|
300
|
+
// still re-check, not trust a value baked in at registration
|
|
301
|
+
// time). When installed, `@opentelemetry/instrumentation-undici`
|
|
302
|
+
// already injects a real, fresh `traceparent` for this exact
|
|
303
|
+
// outbound request (see `otel_propagation.ts`); minting a second
|
|
304
|
+
// one here would put TWO `traceparent` headers on the wire
|
|
305
|
+
// (`UndiciInstrumentation` appends, it does not replace) — this
|
|
306
|
+
// was BLOCKER A. `x-correlation-id`/`x-spf-agent` are NEVER set
|
|
307
|
+
// here either way — see `Model.headers` above (not installed) and
|
|
308
|
+
// `GatewayHeadersPropagator` (installed) for where those two
|
|
309
|
+
// actually come from.
|
|
310
|
+
resolve: async () => ({
|
|
311
|
+
auth: {
|
|
312
|
+
apiKey: ollamaApiKey(),
|
|
313
|
+
...(isFluePropagationInstalled() ? {} : { headers: { traceparent: freshTraceparent() } }),
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
171
316
|
},
|
|
172
317
|
},
|
|
173
318
|
models,
|
|
@@ -205,4 +350,5 @@ export function resetOllamaRegistrationForTest() {
|
|
|
205
350
|
registeredIds.clear();
|
|
206
351
|
inflight.clear();
|
|
207
352
|
lastProvider = undefined;
|
|
353
|
+
registrationContext.clear();
|
|
208
354
|
}
|
package/dist/core/otel.js
CHANGED
|
@@ -407,7 +407,15 @@ export function redact(message, secrets) {
|
|
|
407
407
|
function numOrNull(value) {
|
|
408
408
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
409
409
|
}
|
|
410
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* UsageBreakdown's token fields -> attribute suffixes. Numbers only, by
|
|
412
|
+
* construction. `billable_tokens` rides alongside `total_tokens` — the SAME
|
|
413
|
+
* split `run_dashboard.tsx`'s live spend line and `estimate.ts`'s cutoff
|
|
414
|
+
* projection now both carry (see `UsageBreakdown.billable_tokens`'s doc
|
|
415
|
+
* comment in `data_types.ts`) — so a Langfuse/OTEL consumer graphing spend
|
|
416
|
+
* against `defaults.max_run_tokens` has the metric the real ceiling check
|
|
417
|
+
* actually uses, not just the display total (cache reads included).
|
|
418
|
+
*/
|
|
411
419
|
const TOKEN_FIELDS = [
|
|
412
420
|
["input_tokens", "spf.tokens.input"],
|
|
413
421
|
["output_tokens", "spf.tokens.output"],
|
|
@@ -415,6 +423,7 @@ const TOKEN_FIELDS = [
|
|
|
415
423
|
["cache_write_tokens", "spf.tokens.cache_write"],
|
|
416
424
|
["reasoning_tokens", "spf.tokens.reasoning"],
|
|
417
425
|
["total_tokens", "spf.tokens.total"],
|
|
426
|
+
["billable_tokens", "spf.tokens.billable"],
|
|
418
427
|
];
|
|
419
428
|
const COST_FIELDS = [
|
|
420
429
|
["input_cost", "spf.cost.input"],
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* `-undici` create a real client span (with a real, non-noop SpanContext)
|
|
15
15
|
* around every outbound `http`/`https`/`fetch`(undici) call made from this
|
|
16
16
|
* process, and the OTel API's global propagator is what those
|
|
17
|
-
* instrumentations use to inject `traceparent` (and, here,
|
|
18
|
-
*
|
|
17
|
+
* instrumentations use to inject `traceparent` (and, here, the gateway's own
|
|
18
|
+
* `x-correlation-id`/`x-spf-agent` — see GatewayHeadersPropagator below;
|
|
19
|
+
* NEVER `x-request-id`, which Envoy/Switchyard own end-to-end and which a
|
|
20
|
+
* client-sent value would corrupt) into that call's headers — REGARDLESS of
|
|
21
|
+
* which provider SDK issued it.
|
|
19
22
|
* This reaches every provider whose Node SDK issues requests through
|
|
20
23
|
* Node's own `http`/`https` modules or `undici` (verified: `fetch()`,
|
|
21
24
|
* `https.request()`). It does NOT reach a provider transport that bypasses
|
|
@@ -58,7 +61,64 @@
|
|
|
58
61
|
* agent processing", which is the id SPF mints and hands to
|
|
59
62
|
* `init(SfAgent, { id })`. Extraction goes through the globally
|
|
60
63
|
* registered propagator against ROOT_CONTEXT, so no leaked loop context
|
|
61
|
-
* can stick.
|
|
64
|
+
* can stick.
|
|
65
|
+
*
|
|
66
|
+
* BLOCKER 1 (fixed) — `resolveRootContext`'s return value NEVER becomes the
|
|
67
|
+
* active context. Verified against `@flue/opentelemetry`'s own dist
|
|
68
|
+
* (index.mjs:361 for the `chat <model>` span, :329 for the interceptor that
|
|
69
|
+
* activates it): the resolved `Context` is passed to `tracer.startSpan(...)`
|
|
70
|
+
* ONLY as `parentContext` — it supplies the new span's trace id/parent span
|
|
71
|
+
* id and is then discarded. What actually gets activated around the real
|
|
72
|
+
* dispatch is `context.with(trace.setSpan(context.active(), span), next)` —
|
|
73
|
+
* a context built from `context.active()` (whatever was active before,
|
|
74
|
+
* almost always ROOT) plus the freshly minted `span`, NOT the resolved
|
|
75
|
+
* context itself. A prior version of this module stashed the session's
|
|
76
|
+
* `adw_id`/`agent_name` as a plain context VALUE on the context
|
|
77
|
+
* `resolveFlueRootContext` returned (keyed under a private context key) and
|
|
78
|
+
* had `GatewayHeadersPropagator` read that value back at inject time — which
|
|
79
|
+
* can never work, because that exact context object is never the one made
|
|
80
|
+
* active; only its SpanContext (traceId/spanId/flags) survives, carried by
|
|
81
|
+
* the new span. Confirmed live: `scratchpad/probe_installed_real.mjs`,
|
|
82
|
+
* reproducing flue's exact two-line sequence against a real pi-ai dispatch,
|
|
83
|
+
* printed `x-correlation-id: ABSENT` / `x-spf-agent: ABSENT` before this fix.
|
|
84
|
+
*
|
|
85
|
+
* THE FIX: key the registration by TRACE ID instead of by context identity.
|
|
86
|
+
* `registerFlueSessionTrace` now ALSO records, in `traceIdRegistrations`,
|
|
87
|
+
* the trace id parsed out of the very `traceparent` it's given — the same
|
|
88
|
+
* deterministic trace id `resolveFlueRootContext` extracts and hands to
|
|
89
|
+
* `tracer.startSpan` as that span's parent, so the span it creates carries
|
|
90
|
+
* that exact trace id forward into whatever context DOES get activated.
|
|
91
|
+
* `GatewayHeadersPropagator.inject()` below reads `trace.getSpanContext(ctx)
|
|
92
|
+
* ?.traceId` — the thing that provably survives into the active context at
|
|
93
|
+
* request time — and looks IT UP in `traceIdRegistrations`, rather than
|
|
94
|
+
* trying to read a value off a context object that was never activated.
|
|
95
|
+
* One deterministic trace id per adw_id (`otel.ts:556`'s `traceIdFor(adwId)`,
|
|
96
|
+
* reused for EVERY agent call in that run — `otel.ts:737`'s
|
|
97
|
+
* `agentCallTraceContext` varies only the span id, never the trace id), and
|
|
98
|
+
* `traceIdRegistrations` is keyed on that trace id alone, not on session id
|
|
99
|
+
* or span id. So "latest registration for this trace id wins" is exact
|
|
100
|
+
* within a run when its agents run one at a time — which is how a `fanout`
|
|
101
|
+
* attempt's own agents run: `fanout.ts:448` derives each attempt its OWN
|
|
102
|
+
* adw_id, so distinct fanout attempts get distinct trace ids and cannot
|
|
103
|
+
* collide here, regardless of `fanout`'s concurrency. The one real collision
|
|
104
|
+
* this map can still see is narrower and does not happen in SPF today: TWO
|
|
105
|
+
* AGENTS OVERLAPPING INSIDE ONE adw_id — a single run dispatching a second
|
|
106
|
+
* flue agent call before the first one's `unregisterFlueSessionTrace` has
|
|
107
|
+
* run — since both share that run's one trace id, the second
|
|
108
|
+
* `registerFlueSessionTrace` call overwrites the first agent's entry in
|
|
109
|
+
* `traceIdRegistrations` while its dispatch may still be in flight, and that
|
|
110
|
+
* agent's outbound request would then carry the OTHER agent's
|
|
111
|
+
* `x-correlation-id`/`x-spf-agent` (never a wrong `adw_id`, since both
|
|
112
|
+
* belong to the same run — only the wrong `agentName`). No chain SPF ships
|
|
113
|
+
* dispatches two agents concurrently within one adw_id; this is flagged as
|
|
114
|
+
* the mechanism's honest limit, not a bug being carried forward. The SAME
|
|
115
|
+
* registration carries this session's `adw_id`/agent name (see
|
|
116
|
+
* `FlueSessionRegistration`) so a
|
|
117
|
+
* session's `x-correlation-id`/`x-spf-agent` ride the exact same "resolved
|
|
118
|
+
* once per submission, read per real HTTP call" path as `traceparent` does,
|
|
119
|
+
* via `GatewayHeadersPropagator` below, instead of the static-per-model-id
|
|
120
|
+
* fallback `ollama_provider.ts` needs when this pipeline isn't installed at
|
|
121
|
+
* all (otel unconfigured). Consequences, all intended:
|
|
62
122
|
* - Flue's spans inherit SPF's sha256 trace id, parented under the
|
|
63
123
|
* right agent-call span PER SESSION — correct under multiple agents
|
|
64
124
|
* per process, concurrent agents, and claim-loop restarts alike. Span
|
|
@@ -66,17 +126,42 @@
|
|
|
66
126
|
* - The http/undici client spans' injected `traceparent` carries the
|
|
67
127
|
* deterministic id too, so Switchyard/vLLM hops land as descendants of
|
|
68
128
|
* SPF's trace — parity with `claude_code`'s `ANTHROPIC_CUSTOM_HEADERS`
|
|
69
|
-
* path.
|
|
70
|
-
* correlation
|
|
129
|
+
* path. The same per-request injection point also carries THIS
|
|
130
|
+
* session's `x-correlation-id`/`x-spf-agent`, correctly attributed even
|
|
131
|
+
* when multiple sessions are concurrent in one process, AS LONG AS their
|
|
132
|
+
* deterministic trace ids differ (their `ctx.id`s, and therefore their
|
|
133
|
+
* registrations, are distinct) — see BLOCKER 1 above for the precise
|
|
134
|
+
* lookup key.
|
|
71
135
|
* - Unmapped sessions (never registered, restarted process with a
|
|
72
136
|
* durable backlog, post-`unregister` straggler bookkeeping spans)
|
|
73
137
|
* resolve to an unparented root — flue's spans root a separate SDK
|
|
74
|
-
* trace exactly as v1 did, correlatable by `
|
|
75
|
-
*
|
|
138
|
+
* trace exactly as v1 did, correlatable by `spf.adw_id`/time window.
|
|
139
|
+
* Degraded join, never an error and never MIS-attributed; the gateway
|
|
140
|
+
* headers for exactly this case are simply ABSENT (no fallback identity
|
|
141
|
+
* — see `GatewayHeadersPropagator.inject()` below) rather than guessing
|
|
142
|
+
* at whichever session happened to register most recently.
|
|
76
143
|
* - flue's internal `executionContext.traceCarrier` (typed but not on
|
|
77
144
|
* the public `AgentDispatchRequest` surface) stays unused — noted here
|
|
78
145
|
* as flue's own escape hatch, not something SPF reaches into.
|
|
79
146
|
*
|
|
147
|
+
* STATIC HEADERS STAY SUPPRESSED WHEN INSTALLED (BLOCKER 1's other half,
|
|
148
|
+
* decided AGAINST enabling): `ollama_provider.ts`'s `modelFor()` keeps
|
|
149
|
+
* omitting `Model.headers`' `x-correlation-id`/`x-spf-agent` whenever
|
|
150
|
+
* `isFluePropagationInstalled()` is true — it is NOT also stamped as a
|
|
151
|
+
* baseline alongside this propagator. Measured live
|
|
152
|
+
* (`scratchpad/probe_duplicate_header.mjs`, against the REAL
|
|
153
|
+
* `@opentelemetry/instrumentation-undici` used here): `request.addHeader(k,
|
|
154
|
+
* v)` — what that instrumentation calls for every header
|
|
155
|
+
* `propagation.inject()` returns — APPENDS a second raw header line rather
|
|
156
|
+
* than replacing one already present (undici's own `Request.addHeader` has
|
|
157
|
+
* no dedupe-by-name step); the receiving `http.IncomingMessage.headers`
|
|
158
|
+
* then comma-joins the two into one corrupted value
|
|
159
|
+
* (`"FROM_STATIC, FROM_PROPAGATOR"`), and `req.rawHeaders` shows the literal
|
|
160
|
+
* duplicate line. So when propagation is installed, THIS propagator is the
|
|
161
|
+
* sole source of `x-correlation-id`/`x-spf-agent` — never doubled up with a
|
|
162
|
+
* static value from `Model.headers` — proven on the wire by
|
|
163
|
+
* `src/test/ollama_gateway_e2e.test.ts`'s "no duplicate headers" test.
|
|
164
|
+
*
|
|
80
165
|
* REGISTRATION TIMING. `installFluePropagation()` is called from
|
|
81
166
|
* `agent_flue.ts`'s `run()`, before `ensureRuntime()`/dispatch — i.e. before
|
|
82
167
|
* the actual outbound call, which is the only ordering that matters for
|
|
@@ -98,33 +183,73 @@
|
|
|
98
183
|
* agent dispatches this process makes) happens to arrive first.
|
|
99
184
|
*/
|
|
100
185
|
import { type Context, type TextMapPropagator, type TextMapSetter } from "@opentelemetry/api";
|
|
101
|
-
/** The one custom propagation field this repo adds beyond the standard W3C `traceparent`: the current span's own id, for a collector/log pipeline that correlates by request rather than by trace. */
|
|
102
|
-
export declare const X_REQUEST_ID_HEADER = "x-request-id";
|
|
103
186
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
187
|
+
* The Briefs gateway's own correlation headers — see `ollama_provider.ts`'s
|
|
188
|
+
* "Gateway headers" section for the full three-header contract (the third,
|
|
189
|
+
* `traceparent`, is the W3C standard one `W3CTraceContextPropagator` already
|
|
190
|
+
* injects). Defined here (not in `ollama_provider.ts`) because
|
|
191
|
+
* `GatewayHeadersPropagator` below is the thing that actually injects them
|
|
192
|
+
* onto the wire when this pipeline is installed; `ollama_provider.ts`
|
|
193
|
+
* imports these two constants rather than redeclaring them, so there is
|
|
194
|
+
* exactly one spelling of each header name in the codebase.
|
|
195
|
+
*/
|
|
196
|
+
export declare const X_CORRELATION_ID_HEADER = "x-correlation-id";
|
|
197
|
+
export declare const X_SPF_AGENT_HEADER = "x-spf-agent";
|
|
198
|
+
/**
|
|
199
|
+
* One flue session's (`ctx.id`'s) registration: the W3C `traceparent` string
|
|
200
|
+
* for SPF's deterministic agent-call span (see `registerFlueSessionTrace`),
|
|
201
|
+
* plus this session's `adw_id`/agent name — carried the SAME way, see the
|
|
202
|
+
* module header's FLUE SPANS JOIN... section for why both ride one
|
|
203
|
+
* registration rather than two separate maps.
|
|
204
|
+
*/
|
|
205
|
+
export interface FlueSessionRegistration {
|
|
206
|
+
traceparent: string;
|
|
207
|
+
adwId?: string;
|
|
208
|
+
agentName?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Registers `registration` (SPF's deterministic agent-call traceparent, plus
|
|
212
|
+
* this session's adw_id/agent name — see `FlueSessionRegistration`) as the
|
|
213
|
+
* trace root for flue spans belonging to `sessionId` — flue's instance id,
|
|
214
|
+
* minted by SPF and handed to `init(SfAgent, { id })`. Overwrites a prior
|
|
215
|
+
* registration for the same id (a same-phase retry is the same logical call;
|
|
216
|
+
* the current call wins). Also indexes the SAME identity under this
|
|
217
|
+
* registration's parsed trace id (`traceIdRegistrations`) — see the module
|
|
218
|
+
* header's BLOCKER 1 section and that map's own doc for why.
|
|
109
219
|
*/
|
|
110
|
-
export declare
|
|
220
|
+
export declare function registerFlueSessionTrace(sessionId: string, registration: FlueSessionRegistration): void;
|
|
221
|
+
/**
|
|
222
|
+
* Injects `x-correlation-id`/`x-spf-agent` by looking up the ACTIVE
|
|
223
|
+
* `Context`'s SpanContext trace id (`trace.getSpanContext(ctx)?.traceId`) in
|
|
224
|
+
* `traceIdRegistrations` — the same per-real-HTTP-call injection point
|
|
225
|
+
* `W3CTraceContextPropagator` uses for `traceparent`, and, critically, keyed
|
|
226
|
+
* on the one piece of `resolveFlueRootContext`'s return value that provably
|
|
227
|
+
* survives into that active context (see the module header's BLOCKER 1
|
|
228
|
+
* section for why a context VALUE does not). Injects NOTHING — no fallback,
|
|
229
|
+
* no last-known identity — when this exact trace id is not in the map: an
|
|
230
|
+
* unregistered/unmapped span (flue's own bookkeeping spans, or ANY span
|
|
231
|
+
* after every session has been unregistered) must not stamp a stale or
|
|
232
|
+
* unrelated session's `x-correlation-id`/`x-spf-agent` onto unrelated
|
|
233
|
+
* outbound traffic (a third-party call, a post-session straggler). A true
|
|
234
|
+
* no-op (nothing set) in that case — never throws, never invents a value.
|
|
235
|
+
* Replaces the old `XRequestIdPropagator`:
|
|
236
|
+
* `x-request-id` must NEVER reach the gateway (Envoy/Switchyard own it
|
|
237
|
+
* end-to-end; a client-sent value corrupts their own sampling) and this repo
|
|
238
|
+
* no longer has anything that wants it emitted.
|
|
239
|
+
*/
|
|
240
|
+
export declare class GatewayHeadersPropagator implements TextMapPropagator {
|
|
111
241
|
inject(ctx: Context, carrier: unknown, setter: TextMapSetter): void;
|
|
112
242
|
extract(ctx: Context): Context;
|
|
113
243
|
fields(): string[];
|
|
114
244
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Registers `traceparent` (SPF's deterministic agent-call span, as a W3C
|
|
117
|
-
* carrier string) as the trace root for flue spans belonging to `sessionId`
|
|
118
|
-
* — flue's instance id, minted by SPF and handed to `init(SfAgent, { id })`.
|
|
119
|
-
* Overwrites a prior registration for the same id (a same-phase retry is
|
|
120
|
-
* the same logical call; the current call wins).
|
|
121
|
-
*/
|
|
122
|
-
export declare function registerFlueSessionTrace(sessionId: string, traceparent: string): void;
|
|
123
245
|
/**
|
|
124
246
|
* Idempotent — called from `run()`'s `finally`. Post-settlement bookkeeping
|
|
125
247
|
* spans flue mints after this point simply resolve to an unparented root
|
|
126
248
|
* (separate trace), which is preferable to leaking a registration whose id
|
|
127
|
-
* a REUSED session id could collide with on a later phase.
|
|
249
|
+
* a REUSED session id could collide with on a later phase. Also prunes this
|
|
250
|
+
* session's entry out of `traceIdRegistrations` (parsed fresh from the
|
|
251
|
+
* departing registration's own `traceparent`, so it removes exactly the
|
|
252
|
+
* entry this session added.
|
|
128
253
|
*/
|
|
129
254
|
export declare function unregisterFlueSessionTrace(sessionId: string): void;
|
|
130
255
|
/**
|
|
@@ -137,6 +262,13 @@ export declare function unregisterFlueSessionTrace(sessionId: string): void;
|
|
|
137
262
|
* unparented root span of its own) for an unmapped session id, a malformed
|
|
138
263
|
* traceparent, an absent ctx — and, with no global propagator installed,
|
|
139
264
|
* for everything. Exported for tests.
|
|
265
|
+
*
|
|
266
|
+
* Used by `@flue/opentelemetry` ONLY as `tracer.startSpan`'s parent
|
|
267
|
+
* argument (see the module header's BLOCKER 1 section) — the value handed
|
|
268
|
+
* back here is never itself activated, so it carries no context VALUES for
|
|
269
|
+
* `GatewayHeadersPropagator` to read; `x-correlation-id`/`x-spf-agent` are
|
|
270
|
+
* instead looked up by trace id, via `traceIdRegistrations`, which
|
|
271
|
+
* `registerFlueSessionTrace` populates independently of this function.
|
|
140
272
|
*/
|
|
141
273
|
export declare function resolveFlueRootContext(_event: unknown, ctx: {
|
|
142
274
|
id?: string;
|
|
@@ -157,3 +289,15 @@ export interface FluePropagationConfig {
|
|
|
157
289
|
export declare function installFluePropagation(cfg: FluePropagationConfig | undefined | null, log?: (message: string) => void): void;
|
|
158
290
|
/** Tests only: forget global installation state. Does NOT undo `setGlobalTracerProvider`/`registerInstrumentations` (the OTel API has no supported "un-register" — tests that need isolation run in a fresh process). */
|
|
159
291
|
export declare function resetFluePropagationForTest(): void;
|
|
292
|
+
/**
|
|
293
|
+
* Whether `installFluePropagation` has actually installed the global
|
|
294
|
+
* TracerProvider/propagator/instrumentations in THIS process. Consulted by
|
|
295
|
+
* `ollama_provider.ts` (see its "Gateway headers" section) so `resolve()`/
|
|
296
|
+
* `modelFor()` know whether the instrumentation above already covers
|
|
297
|
+
* `traceparent`/`x-correlation-id`/`x-spf-agent` per real outbound call —
|
|
298
|
+
* supplying any of the three a second way when this is `true` would double
|
|
299
|
+
* it up on the wire (`UndiciInstrumentation` appends via `addHeader`, it
|
|
300
|
+
* does not replace). `false` — the common case, otel unconfigured — means
|
|
301
|
+
* `ollama_provider.ts` must supply all three itself.
|
|
302
|
+
*/
|
|
303
|
+
export declare function isFluePropagationInstalled(): boolean;
|