@gr8ful/spf 0.18.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 +58 -3
- package/dist/core/issues/jira_provider.js +89 -18
- package/dist/core/issues/markdown_adf.d.ts +58 -0
- package/dist/core/issues/markdown_adf.js +705 -0
- 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 +3 -3
|
@@ -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;
|
|
@@ -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
|
|
@@ -108,56 +193,124 @@ import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trac
|
|
|
108
193
|
import { createOpenTelemetryInstrumentation } from "@flue/opentelemetry";
|
|
109
194
|
import { instrument } from "@flue/runtime";
|
|
110
195
|
import { resolveTracesUrl } from "./otel.js";
|
|
111
|
-
/** 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. */
|
|
112
|
-
export const X_REQUEST_ID_HEADER = "x-request-id";
|
|
113
196
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
197
|
+
* The Briefs gateway's own correlation headers — see `ollama_provider.ts`'s
|
|
198
|
+
* "Gateway headers" section for the full three-header contract (the third,
|
|
199
|
+
* `traceparent`, is the W3C standard one `W3CTraceContextPropagator` already
|
|
200
|
+
* injects). Defined here (not in `ollama_provider.ts`) because
|
|
201
|
+
* `GatewayHeadersPropagator` below is the thing that actually injects them
|
|
202
|
+
* onto the wire when this pipeline is installed; `ollama_provider.ts`
|
|
203
|
+
* imports these two constants rather than redeclaring them, so there is
|
|
204
|
+
* exactly one spelling of each header name in the codebase.
|
|
205
|
+
*/
|
|
206
|
+
export const X_CORRELATION_ID_HEADER = "x-correlation-id";
|
|
207
|
+
export const X_SPF_AGENT_HEADER = "x-spf-agent";
|
|
208
|
+
/**
|
|
209
|
+
* Instance-id -> registration backing `resolveFlueRootContext` — see the
|
|
210
|
+
* module header for the full mechanism and WHY this is a map consulted per
|
|
211
|
+
* span rather than a dispatch-time context wrap (flue's single claim loop
|
|
212
|
+
* makes the latter mis-attribute every agent after the first). One entry per
|
|
213
|
+
* in-flight `agent_flue.run()` call.
|
|
214
|
+
*/
|
|
215
|
+
const flueSessionTraces = new Map();
|
|
216
|
+
/**
|
|
217
|
+
* Trace id -> gateway identity, keyed on the deterministic trace id parsed
|
|
218
|
+
* out of a registration's OWN `traceparent` (see `traceIdFromTraceparent`) —
|
|
219
|
+
* see the module header's BLOCKER 1 section for why this, and not a context
|
|
220
|
+
* VALUE, is the lookup `GatewayHeadersPropagator` uses. Populated by
|
|
221
|
+
* `registerFlueSessionTrace`, pruned by `unregisterFlueSessionTrace`. A
|
|
222
|
+
* trace id collision here (two DIFFERENT registrations sharing one trace id)
|
|
223
|
+
* is not expected in practice — trace ids are SPF's deterministic per-adw_id
|
|
224
|
+
* ids — but if it ever happened, the later `registerFlueSessionTrace` call
|
|
225
|
+
* would simply win, matching this map's own "current call wins" rule.
|
|
119
226
|
*/
|
|
120
|
-
|
|
227
|
+
const traceIdRegistrations = new Map();
|
|
228
|
+
/**
|
|
229
|
+
* Parses the W3C `traceId` segment out of a `traceparent` string via a real
|
|
230
|
+
* `propagation.extract`/`getSpanContext` round trip — the SAME extraction
|
|
231
|
+
* `resolveFlueRootContext` performs, so "the trace id we index
|
|
232
|
+
* `traceIdRegistrations` under" and "the trace id a span parented via this
|
|
233
|
+
* `traceparent` actually carries" are provably the same value, not two
|
|
234
|
+
* independent parses that could drift. Returns `undefined` for a malformed
|
|
235
|
+
* `traceparent` (mirrors `resolveFlueRootContext`'s own malformed-input
|
|
236
|
+
* handling) rather than throwing.
|
|
237
|
+
*/
|
|
238
|
+
function traceIdFromTraceparent(traceparent) {
|
|
239
|
+
const extracted = propagation.extract(ROOT_CONTEXT, { traceparent }, defaultTextMapGetter);
|
|
240
|
+
const spanContext = traceApi.getSpanContext(extracted);
|
|
241
|
+
return spanContext && isSpanContextValid(spanContext) ? spanContext.traceId : undefined;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Registers `registration` (SPF's deterministic agent-call traceparent, plus
|
|
245
|
+
* this session's adw_id/agent name — see `FlueSessionRegistration`) as the
|
|
246
|
+
* trace root for flue spans belonging to `sessionId` — flue's instance id,
|
|
247
|
+
* minted by SPF and handed to `init(SfAgent, { id })`. Overwrites a prior
|
|
248
|
+
* registration for the same id (a same-phase retry is the same logical call;
|
|
249
|
+
* the current call wins). Also indexes the SAME identity under this
|
|
250
|
+
* registration's parsed trace id (`traceIdRegistrations`) — see the module
|
|
251
|
+
* header's BLOCKER 1 section and that map's own doc for why.
|
|
252
|
+
*/
|
|
253
|
+
export function registerFlueSessionTrace(sessionId, registration) {
|
|
254
|
+
flueSessionTraces.set(sessionId, registration);
|
|
255
|
+
const identity = { adwId: registration.adwId, agentName: registration.agentName };
|
|
256
|
+
const traceId = traceIdFromTraceparent(registration.traceparent);
|
|
257
|
+
if (traceId)
|
|
258
|
+
traceIdRegistrations.set(traceId, identity);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Injects `x-correlation-id`/`x-spf-agent` by looking up the ACTIVE
|
|
262
|
+
* `Context`'s SpanContext trace id (`trace.getSpanContext(ctx)?.traceId`) in
|
|
263
|
+
* `traceIdRegistrations` — the same per-real-HTTP-call injection point
|
|
264
|
+
* `W3CTraceContextPropagator` uses for `traceparent`, and, critically, keyed
|
|
265
|
+
* on the one piece of `resolveFlueRootContext`'s return value that provably
|
|
266
|
+
* survives into that active context (see the module header's BLOCKER 1
|
|
267
|
+
* section for why a context VALUE does not). Injects NOTHING — no fallback,
|
|
268
|
+
* no last-known identity — when this exact trace id is not in the map: an
|
|
269
|
+
* unregistered/unmapped span (flue's own bookkeeping spans, or ANY span
|
|
270
|
+
* after every session has been unregistered) must not stamp a stale or
|
|
271
|
+
* unrelated session's `x-correlation-id`/`x-spf-agent` onto unrelated
|
|
272
|
+
* outbound traffic (a third-party call, a post-session straggler). A true
|
|
273
|
+
* no-op (nothing set) in that case — never throws, never invents a value.
|
|
274
|
+
* Replaces the old `XRequestIdPropagator`:
|
|
275
|
+
* `x-request-id` must NEVER reach the gateway (Envoy/Switchyard own it
|
|
276
|
+
* end-to-end; a client-sent value corrupts their own sampling) and this repo
|
|
277
|
+
* no longer has anything that wants it emitted.
|
|
278
|
+
*/
|
|
279
|
+
export class GatewayHeadersPropagator {
|
|
121
280
|
inject(ctx, carrier, setter) {
|
|
122
281
|
const spanContext = traceApi.getSpanContext(ctx);
|
|
123
|
-
|
|
282
|
+
const registration = spanContext && traceIdRegistrations.get(spanContext.traceId);
|
|
283
|
+
if (!registration)
|
|
124
284
|
return;
|
|
125
|
-
|
|
285
|
+
if (registration.adwId)
|
|
286
|
+
setter.set(carrier, X_CORRELATION_ID_HEADER, registration.adwId);
|
|
287
|
+
if (registration.agentName)
|
|
288
|
+
setter.set(carrier, X_SPF_AGENT_HEADER, registration.agentName);
|
|
126
289
|
}
|
|
127
290
|
extract(ctx) {
|
|
128
291
|
return ctx;
|
|
129
292
|
}
|
|
130
293
|
fields() {
|
|
131
|
-
return [
|
|
294
|
+
return [X_CORRELATION_ID_HEADER, X_SPF_AGENT_HEADER];
|
|
132
295
|
}
|
|
133
296
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Instance-id -> agent-call traceparent registrations backing
|
|
136
|
-
* `resolveFlueRootContext` — see the module header for the full mechanism
|
|
137
|
-
* and WHY this is a map consulted per span rather than a dispatch-time
|
|
138
|
-
* context wrap (flue's single claim loop makes the latter mis-attribute
|
|
139
|
-
* every agent after the first). One entry per in-flight `agent_flue.run()`
|
|
140
|
-
* call.
|
|
141
|
-
*/
|
|
142
|
-
const flueSessionTraces = new Map();
|
|
143
|
-
/**
|
|
144
|
-
* Registers `traceparent` (SPF's deterministic agent-call span, as a W3C
|
|
145
|
-
* carrier string) as the trace root for flue spans belonging to `sessionId`
|
|
146
|
-
* — flue's instance id, minted by SPF and handed to `init(SfAgent, { id })`.
|
|
147
|
-
* Overwrites a prior registration for the same id (a same-phase retry is
|
|
148
|
-
* the same logical call; the current call wins).
|
|
149
|
-
*/
|
|
150
|
-
export function registerFlueSessionTrace(sessionId, traceparent) {
|
|
151
|
-
flueSessionTraces.set(sessionId, traceparent);
|
|
152
|
-
}
|
|
153
297
|
/**
|
|
154
298
|
* Idempotent — called from `run()`'s `finally`. Post-settlement bookkeeping
|
|
155
299
|
* spans flue mints after this point simply resolve to an unparented root
|
|
156
300
|
* (separate trace), which is preferable to leaking a registration whose id
|
|
157
|
-
* a REUSED session id could collide with on a later phase.
|
|
301
|
+
* a REUSED session id could collide with on a later phase. Also prunes this
|
|
302
|
+
* session's entry out of `traceIdRegistrations` (parsed fresh from the
|
|
303
|
+
* departing registration's own `traceparent`, so it removes exactly the
|
|
304
|
+
* entry this session added.
|
|
158
305
|
*/
|
|
159
306
|
export function unregisterFlueSessionTrace(sessionId) {
|
|
307
|
+
const registration = flueSessionTraces.get(sessionId);
|
|
160
308
|
flueSessionTraces.delete(sessionId);
|
|
309
|
+
if (!registration)
|
|
310
|
+
return;
|
|
311
|
+
const traceId = traceIdFromTraceparent(registration.traceparent);
|
|
312
|
+
if (traceId)
|
|
313
|
+
traceIdRegistrations.delete(traceId);
|
|
161
314
|
}
|
|
162
315
|
/**
|
|
163
316
|
* The `resolveRootContext` implementation handed to
|
|
@@ -169,14 +322,23 @@ export function unregisterFlueSessionTrace(sessionId) {
|
|
|
169
322
|
* unparented root span of its own) for an unmapped session id, a malformed
|
|
170
323
|
* traceparent, an absent ctx — and, with no global propagator installed,
|
|
171
324
|
* for everything. Exported for tests.
|
|
325
|
+
*
|
|
326
|
+
* Used by `@flue/opentelemetry` ONLY as `tracer.startSpan`'s parent
|
|
327
|
+
* argument (see the module header's BLOCKER 1 section) — the value handed
|
|
328
|
+
* back here is never itself activated, so it carries no context VALUES for
|
|
329
|
+
* `GatewayHeadersPropagator` to read; `x-correlation-id`/`x-spf-agent` are
|
|
330
|
+
* instead looked up by trace id, via `traceIdRegistrations`, which
|
|
331
|
+
* `registerFlueSessionTrace` populates independently of this function.
|
|
172
332
|
*/
|
|
173
333
|
export function resolveFlueRootContext(_event, ctx) {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
334
|
+
const registration = ctx?.id ? flueSessionTraces.get(ctx.id) : undefined;
|
|
335
|
+
if (!registration)
|
|
176
336
|
return undefined;
|
|
177
|
-
const extracted = propagation.extract(ROOT_CONTEXT, { traceparent }, defaultTextMapGetter);
|
|
337
|
+
const extracted = propagation.extract(ROOT_CONTEXT, { traceparent: registration.traceparent }, defaultTextMapGetter);
|
|
178
338
|
const spanContext = traceApi.getSpanContext(extracted);
|
|
179
|
-
|
|
339
|
+
if (!spanContext || !isSpanContextValid(spanContext))
|
|
340
|
+
return undefined;
|
|
341
|
+
return extracted;
|
|
180
342
|
}
|
|
181
343
|
let installed = false;
|
|
182
344
|
let flueInstrumented = false;
|
|
@@ -200,7 +362,7 @@ export function installFluePropagation(cfg, log = (m) => console.error(m)) {
|
|
|
200
362
|
});
|
|
201
363
|
traceApi.setGlobalTracerProvider(provider);
|
|
202
364
|
contextApi.setGlobalContextManager(new AsyncHooksContextManager().enable());
|
|
203
|
-
propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new
|
|
365
|
+
propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new GatewayHeadersPropagator()] }));
|
|
204
366
|
registerInstrumentations({ instrumentations: [new HttpInstrumentation(), new UndiciInstrumentation()] });
|
|
205
367
|
// Set AFTER the fallible registrations above: on a construction-time
|
|
206
368
|
// throw, the next call must be free to retry — latching `installed`
|
|
@@ -223,3 +385,17 @@ export function resetFluePropagationForTest() {
|
|
|
223
385
|
installed = false;
|
|
224
386
|
flueInstrumented = false;
|
|
225
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Whether `installFluePropagation` has actually installed the global
|
|
390
|
+
* TracerProvider/propagator/instrumentations in THIS process. Consulted by
|
|
391
|
+
* `ollama_provider.ts` (see its "Gateway headers" section) so `resolve()`/
|
|
392
|
+
* `modelFor()` know whether the instrumentation above already covers
|
|
393
|
+
* `traceparent`/`x-correlation-id`/`x-spf-agent` per real outbound call —
|
|
394
|
+
* supplying any of the three a second way when this is `true` would double
|
|
395
|
+
* it up on the wire (`UndiciInstrumentation` appends via `addHeader`, it
|
|
396
|
+
* does not replace). `false` — the common case, otel unconfigured — means
|
|
397
|
+
* `ollama_provider.ts` must supply all three itself.
|
|
398
|
+
*/
|
|
399
|
+
export function isFluePropagationInstalled() {
|
|
400
|
+
return installed;
|
|
401
|
+
}
|
|
@@ -73,6 +73,21 @@ export declare function permitted(p: string, agent: AgentConfig, cfg: SFConfig):
|
|
|
73
73
|
* Detection alone would leave the repo holding the unauthorized change while
|
|
74
74
|
* reporting a failure, so anything the agent introduced outside its allowlist
|
|
75
75
|
* is rolled back before the phase dies. What it cannot undo, it names.
|
|
76
|
+
*
|
|
77
|
+
* `defaults.read_only_ignore` carves out one exception to "names, fails" —
|
|
78
|
+
* but only where `isSafeToIgnore` says restoring is actually possible (see
|
|
79
|
+
* its own doc comment): a TRUE read-only agent (`writes: []`) that churned
|
|
80
|
+
* a lockfile that was CLEAN before this phase. That one case is STILL
|
|
81
|
+
* rolled back unconditionally like any other breach — an ignored path is
|
|
82
|
+
* never left standing — it just does not, on its own, fail the phase.
|
|
83
|
+
* Everything else `isIgnorableChurn` alone would have matched (a dirty-before
|
|
84
|
+
* path, an agent that reverted uncommitted work, a write-restricted rather
|
|
85
|
+
* than read-only agent) falls through to the real-breach path below instead.
|
|
86
|
+
* `onIgnored`, when given, is called once with every safely-ignored path
|
|
87
|
+
* before returning, so a caller with a logger (`agents.ts`'s `execute()`)
|
|
88
|
+
* can print the one required info line — this module has no logger of its
|
|
89
|
+
* own to call (`RunLike` is only `repo_root`/`cfg`), so the caller does the
|
|
90
|
+
* printing.
|
|
76
91
|
*/
|
|
77
|
-
export declare function enforce(run: RunLike, _phase: unknown, agent: AgentConfig, before: Record<string, string
|
|
92
|
+
export declare function enforce(run: RunLike, _phase: unknown, agent: AgentConfig, before: Record<string, string>, onIgnored?: (paths: string[]) => void): string[];
|
|
78
93
|
export {};
|