@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
package/dist/core/data_types.js
CHANGED
|
@@ -528,6 +528,46 @@ export const ConfigDefaultsSchema = v.object({
|
|
|
528
528
|
// the machinery that decides whether its work passed.
|
|
529
529
|
// .spf/ is the whole per-repo footprint now — no adws/ tree to protect.
|
|
530
530
|
protected_files: v.optional(v.array(v.string()), () => [".spf/", "spf.config.yaml"]),
|
|
531
|
+
/**
|
|
532
|
+
* Paths a READ-ONLY (or write-restricted) agent may touch WITHOUT failing
|
|
533
|
+
* the phase — `core/permissions.ts`'s `enforce()` still rolls every one of
|
|
534
|
+
* them back (an agent's claimed report must never rest on a change that
|
|
535
|
+
* didn't survive), it just does not count that rollback as a breach.
|
|
536
|
+
*
|
|
537
|
+
* WHY THIS EXISTS: a lockfile is dependency-manager BOOKKEEPING, not the
|
|
538
|
+
* repo's intent — an agent that ran `npm install` (to read a package's
|
|
539
|
+
* real shape, say) rewrites `package-lock.json` as a side effect of a
|
|
540
|
+
* read, not an edit. Observed live: a read-only scout phase failed with
|
|
541
|
+
* "scout is read-only but modified 1 path(s): factory/content/
|
|
542
|
+
* package-lock.json — rolled back" over exactly this, for work that
|
|
543
|
+
* changed nothing an operator would call "the code."
|
|
544
|
+
*
|
|
545
|
+
* The four defaults are the lockfiles of every package manager this repo
|
|
546
|
+
* already builds against (npm, pnpm, yarn, bun) — additive, not
|
|
547
|
+
* exhaustive; a repo using another one adds its own pattern here. Same
|
|
548
|
+
* glob syntax as `protected_files`/`agents[].writes` (`permissions.ts`'s
|
|
549
|
+
* `globToRegex`), where a leading "**" followed by a path separator
|
|
550
|
+
* matches at any depth INCLUDING the repo root — so the packaged
|
|
551
|
+
* defaults below match a lockfile whether it sits at the top of the repo
|
|
552
|
+
* or nested under a subdirectory.
|
|
553
|
+
*
|
|
554
|
+
* Emptying this list (`read_only_ignore: []`) restores today's strict
|
|
555
|
+
* behavior exactly — every touched path outside an agent's own allowlist
|
|
556
|
+
* fails the phase, lockfiles included.
|
|
557
|
+
*
|
|
558
|
+
* PRECEDENCE: `protected_files` always wins. A path matching
|
|
559
|
+
* `protected_files` is never ignorable via `read_only_ignore`, no matter
|
|
560
|
+
* how narrowly write-restricted the agent is — a pattern here that
|
|
561
|
+
* happens to also match a protected path is not read as "exempt this
|
|
562
|
+
* from protected_files too"; it stays a real breach. See
|
|
563
|
+
* `permissions.ts`'s `isSafeToIgnore` for the enforcement.
|
|
564
|
+
*/
|
|
565
|
+
read_only_ignore: v.optional(v.array(v.string()), () => [
|
|
566
|
+
"**/package-lock.json",
|
|
567
|
+
"**/pnpm-lock.yaml",
|
|
568
|
+
"**/yarn.lock",
|
|
569
|
+
"**/bun.lockb",
|
|
570
|
+
]),
|
|
531
571
|
data_dir: v.optional(v.string(), ".spf/data"),
|
|
532
572
|
/**
|
|
533
573
|
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
@@ -550,11 +590,20 @@ export const ConfigDefaultsSchema = v.object({
|
|
|
550
590
|
* `agents.ts`'s `BudgetExceeded`.
|
|
551
591
|
*
|
|
552
592
|
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
553
|
-
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is
|
|
554
|
-
* tokens
|
|
555
|
-
*
|
|
556
|
-
* `
|
|
557
|
-
*
|
|
593
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is BILLABLE
|
|
594
|
+
* tokens — `UsageBreakdown.billable_tokens` (input + cache-write + output),
|
|
595
|
+
* checked against `Run.billable_tokens`, NOT the `total_tokens` column
|
|
596
|
+
* `sessions` also carries for display. A prompt-caching backend (Ollama
|
|
597
|
+
* Cloud's kimi models, Anthropic's own caching) re-sends the whole
|
|
598
|
+
* conversation every turn as CACHE READS, which `total_tokens` counts and
|
|
599
|
+
* this ceiling does not: cache reads are billed (when billed at all) at a
|
|
600
|
+
* small fraction of input price, sometimes free, so a ceiling measured
|
|
601
|
+
* against the bigger number trips on bulk that cost nothing — observed
|
|
602
|
+
* live, one scout phase alone reported 1,311,740 total_tokens against a
|
|
603
|
+
* gateway that billed 189,321 uncached input + 17,908 output for it.
|
|
604
|
+
* `total_tokens` is kept exactly as before for anything display-only
|
|
605
|
+
* (the sessions-panel "tokens" line, the UI) — only the budget check
|
|
606
|
+
* changed which number it reads.
|
|
558
607
|
*
|
|
559
608
|
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
560
609
|
* run", which is a config mistake, not a budget — it would fail the first
|
|
@@ -573,6 +622,43 @@ export const ConfigDefaultsSchema = v.object({
|
|
|
573
622
|
*/
|
|
574
623
|
max_run_cost: v.optional(v.pipe(v.number(), v.gtValue(0))),
|
|
575
624
|
max_run_tokens: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0))),
|
|
625
|
+
/**
|
|
626
|
+
* REQUEST TIMEOUT — how long a single agent dispatch may run before it is
|
|
627
|
+
* aborted and settled as failed, rather than hanging on a connection that
|
|
628
|
+
* silently died mid-call with nothing to notice.
|
|
629
|
+
*
|
|
630
|
+
* FLUE-SPECIFIC, unlike every other key in this schema: it maps straight
|
|
631
|
+
* onto `AgentStatics.durability.timeoutMs` (see `@flue/runtime`'s own
|
|
632
|
+
* docs), a `flue`-backend-only mechanism. `claude_code`/`opencode` are
|
|
633
|
+
* subprocess backends with no such knob today — this field is silently
|
|
634
|
+
* ignored for them, the same way `flue_db_path` on `AgentRequest` already
|
|
635
|
+
* is. Not a bug to fix here: a subprocess backend needs its own separate
|
|
636
|
+
* process-level timeout story, which is out of scope for this key.
|
|
637
|
+
*
|
|
638
|
+
* ABSENT BY DEFAULT, and absence is a total no-op: Flue's own default
|
|
639
|
+
* applies unchanged (1 hour, 10 attempts) — the same "surprise mid-run
|
|
640
|
+
* failure on a ceiling nobody chose is worse than the spend" reasoning as
|
|
641
|
+
* `max_run_cost`/`max_run_tokens` above. Set this when a hung connection
|
|
642
|
+
* should surface as an attributable failure (and feed the normal
|
|
643
|
+
* gate-correction / `spf watch` retry loop) in minutes, not however long
|
|
644
|
+
* Flue's own default takes — e.g. `300_000` for a five-minute ceiling.
|
|
645
|
+
* NOT a precise deadline, though: manual verification against a socket
|
|
646
|
+
* that accepts a connection and then sends nothing (see
|
|
647
|
+
* `request_timeout.test.ts`'s header comment) saw Flue's own timeout check
|
|
648
|
+
* fire on a coarser periodic sweep — a 3s ceiling settled at ~15s, not 3s.
|
|
649
|
+
* Bounded-but-imprecise is still a firm improvement over unbounded.
|
|
650
|
+
*
|
|
651
|
+
* SCOPE IS ONE SUBMISSION (one agent dispatch — the first prompt, one
|
|
652
|
+
* JSON-repair retry, one gate correction), NOT the accumulated run, unlike
|
|
653
|
+
* `max_run_cost`/`max_run_tokens` above. It is still process-scoped, not
|
|
654
|
+
* per-agent: Flue's `durability` is a static on the single shared agent
|
|
655
|
+
* function `agent_flue.ts` dispatches everything through, set once before
|
|
656
|
+
* the first dispatch of the process — see that file's `ensureRuntime()`.
|
|
657
|
+
* Deliberately NOT in `loadConfig`'s per-agent back-fill list for the same
|
|
658
|
+
* reason `max_run_cost`/`max_run_tokens` aren't: a per-agent copy would
|
|
659
|
+
* read as "this agent gets its own timeout", which nothing enforces.
|
|
660
|
+
*/
|
|
661
|
+
request_timeout_ms: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0))),
|
|
576
662
|
});
|
|
577
663
|
/**
|
|
578
664
|
* OpenTelemetry span export — OFF unless this block exists, and `endpoint` is
|
|
@@ -869,6 +955,30 @@ export const WatchJiraConfigSchema = v.object({
|
|
|
869
955
|
project_key: v.optional(v.string(), ""), // e.g. "PROJ"
|
|
870
956
|
issue_types: v.optional(JiraIssueTypeMapSchema, () => v.parse(JiraIssueTypeMapSchema, {})),
|
|
871
957
|
status_map: v.optional(JiraStatusMapSchema, () => v.parse(JiraStatusMapSchema, {})),
|
|
958
|
+
/**
|
|
959
|
+
* The Jira issue-link `type` name `refine.ts`'s `publish()` uses to
|
|
960
|
+
* connect a freshly-published tree's ROOT issue(s) back to the spec they
|
|
961
|
+
* were refined from (`JiraProvider.linkToSpec`) — a plain, symmetric
|
|
962
|
+
* "issue link" (Jira's generic relate-two-issues mechanism), never the
|
|
963
|
+
* hierarchical `parent` field `linkChild` sets: the spec's own issue type
|
|
964
|
+
* defaults to Story (`issue_types.spec`), and a root node is often an
|
|
965
|
+
* Epic/Task — Jira's issue-type hierarchy frequently refuses a Story as
|
|
966
|
+
* one of those types' PARENT, so the hierarchy field is not a safe choice
|
|
967
|
+
* here regardless of which type actually published. "Relates" is a
|
|
968
|
+
* built-in link type on every Jira Cloud project; override this only if a
|
|
969
|
+
* project's admin has renamed or restricted it.
|
|
970
|
+
*
|
|
971
|
+
* MUST NAME A SYMMETRIC LINK TYPE. `JiraProvider.linkToSpec` fixes which
|
|
972
|
+
* side is `inwardIssue`/`outwardIssue` (the published root is always
|
|
973
|
+
* inward, the spec always outward) and does not expose direction as a
|
|
974
|
+
* separate knob — harmless for a symmetric type like "Relates" (Jira's UI
|
|
975
|
+
* does not even surface a direction for one), but pointing this at a
|
|
976
|
+
* DIRECTIONAL type (e.g. "blocks"/"is blocked by") would silently record
|
|
977
|
+
* the opposite relationship from the one intended. Only rename this to
|
|
978
|
+
* another symmetric type; a directional one needs code changes, not just
|
|
979
|
+
* config.
|
|
980
|
+
*/
|
|
981
|
+
link_type: v.optional(v.string(), "Relates"),
|
|
872
982
|
});
|
|
873
983
|
/**
|
|
874
984
|
* Optional, per-repo `WatchState` -> GitHub Projects v2 "Status" option-name
|
|
@@ -1230,6 +1340,24 @@ export class UsageBreakdown {
|
|
|
1230
1340
|
// at the output rate. Report it nested under output, never added to it.
|
|
1231
1341
|
reasoning_tokens = 0;
|
|
1232
1342
|
total_tokens = 0;
|
|
1343
|
+
/**
|
|
1344
|
+
* The SPEND number, as distinct from `total_tokens` (the SIZE number).
|
|
1345
|
+
* `input_tokens + output_tokens + cache_write_tokens` — `cache_read_tokens`
|
|
1346
|
+
* excluded on purpose: a cache read is Anthropic's own prompt-caching
|
|
1347
|
+
* discount (billed at a small fraction of the input rate, sometimes free
|
|
1348
|
+
* on some gateways) for context the conversation already sent, not new
|
|
1349
|
+
* material moved. `total_tokens` re-sends (and re-counts) the whole
|
|
1350
|
+
* conversation every turn, so a long-running scout/build session's cache
|
|
1351
|
+
* reads dwarf everything else in it (observed live: 1.31M total_tokens in
|
|
1352
|
+
* one phase, of which 1.15M were cache reads the gateway did not bill as
|
|
1353
|
+
* input) — a run-budget ceiling measured against `total_tokens` trips on
|
|
1354
|
+
* cache-driven bulk that cost nothing, not on real spend. `assertRunBudget`
|
|
1355
|
+
* (`agents.ts`) checks THIS field against `defaults.max_run_tokens`;
|
|
1356
|
+
* `total_tokens` is kept, unchanged, for display (the sessions-panel
|
|
1357
|
+
* "tokens" line, the UI) because an operator sizing context occupancy
|
|
1358
|
+
* still needs the real re-send count, not the billable one.
|
|
1359
|
+
*/
|
|
1360
|
+
billable_tokens = 0;
|
|
1233
1361
|
input_cost = 0.0;
|
|
1234
1362
|
output_cost = 0.0;
|
|
1235
1363
|
cache_read_cost = 0.0;
|
|
@@ -1249,6 +1377,7 @@ export class UsageBreakdown {
|
|
|
1249
1377
|
this.cache_write_tokens += usage.cacheWrite || 0;
|
|
1250
1378
|
this.reasoning_tokens += usage.reasoning || 0;
|
|
1251
1379
|
this.total_tokens += totalTokens;
|
|
1380
|
+
this.billable_tokens += (usage.input || 0) + (usage.output || 0) + (usage.cacheWrite || 0);
|
|
1252
1381
|
this.input_cost += cost.input || 0.0;
|
|
1253
1382
|
this.output_cost += cost.output || 0.0;
|
|
1254
1383
|
this.cache_read_cost += cost.cacheRead || 0.0;
|
|
@@ -71,6 +71,15 @@
|
|
|
71
71
|
* `feature` (both mapping to Jira's Epic type by default) surfaces a real
|
|
72
72
|
* Jira API error at publish time — a genuine platform difference, not
|
|
73
73
|
* something this file tries to paper over.
|
|
74
|
+
*
|
|
75
|
+
* `linkToSpec` is the OTHER half of `IssueAuthoringProvider`'s hierarchy —
|
|
76
|
+
* a published tree's ROOT connected back to the spec it was refined FROM,
|
|
77
|
+
* which `linkChild` cannot express (the spec's own issue type, a Story by
|
|
78
|
+
* default, frequently cannot legally PARENT a root node's type). Uses
|
|
79
|
+
* Jira's plain issue-link API instead (`type` configurable via
|
|
80
|
+
* `watch.jira.link_type`, "Relates" by default), with a plain comment as
|
|
81
|
+
* its own fallback if that API is unavailable — see the method's own doc
|
|
82
|
+
* comment.
|
|
74
83
|
*/
|
|
75
84
|
import type { JiraIssueTypeMap, JiraStatusMap } from "../data_types.ts";
|
|
76
85
|
import type { EnsureLabelsResult, Issue, IssueAuthoringKind, IssueAuthoringProvider, IssueComment, IssueProvider, WatchMarker, WatchState } from "./provider.ts";
|
|
@@ -82,8 +91,9 @@ export declare class JiraProvider implements IssueProvider, IssueAuthoringProvid
|
|
|
82
91
|
private readonly apiToken;
|
|
83
92
|
private readonly issueTypes;
|
|
84
93
|
private readonly statusMap;
|
|
94
|
+
private readonly linkType;
|
|
85
95
|
constructor(baseUrl: string, // e.g. "https://your-domain.atlassian.net", no trailing slash
|
|
86
|
-
projectKey: string, labelPrefix: string, email: string, apiToken: string, issueTypes: JiraIssueTypeMap, statusMap?: JiraStatusMap);
|
|
96
|
+
projectKey: string, labelPrefix: string, email: string, apiToken: string, issueTypes: JiraIssueTypeMap, statusMap?: JiraStatusMap, linkType?: string);
|
|
87
97
|
private authHeader;
|
|
88
98
|
private jira;
|
|
89
99
|
private label;
|
|
@@ -134,6 +144,46 @@ export declare class JiraProvider implements IssueProvider, IssueAuthoringProvid
|
|
|
134
144
|
* Epic-under-Epic limitation this implies.
|
|
135
145
|
*/
|
|
136
146
|
linkChild(parent: Issue, child: Issue): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* `IssueAuthoringProvider.linkToSpec` — see its own doc comment
|
|
149
|
+
* (`issues/provider.ts`) for why this is a plain Jira "issue link"
|
|
150
|
+
* (`/rest/api/3/issueLink`) rather than `linkChild`'s hierarchy `parent`
|
|
151
|
+
* field: the spec's issue type (Story by default) frequently cannot
|
|
152
|
+
* legally PARENT a root node's type under Jira's issue-type hierarchy,
|
|
153
|
+
* while a generic issue link has no such restriction.
|
|
154
|
+
*
|
|
155
|
+
* `inwardIssue`/`outwardIssue` direction is arbitrary for a symmetric
|
|
156
|
+
* type like "Relates" — Jira does not surface it differently in the UI —
|
|
157
|
+
* so `issue` (the freshly published root) is the inward side and the
|
|
158
|
+
* spec is the outward side, consistently.
|
|
159
|
+
*
|
|
160
|
+
* BEST-EFFORT: this project's Jira instance may not have `linkType`
|
|
161
|
+
* enabled/named exactly this way (a renamed or removed link type, a
|
|
162
|
+
* permission scheme that disallows issue links for this project, ...) —
|
|
163
|
+
* a failure here must never fail the whole publish over what is, at
|
|
164
|
+
* bottom, a cosmetic cross-reference. Falls back to a plain comment
|
|
165
|
+
* naming the spec, so the relationship is visible SOMEWHERE even when
|
|
166
|
+
* the link API itself is unavailable.
|
|
167
|
+
*
|
|
168
|
+
* The fallback comment gets the SAME "never fail publish() over this"
|
|
169
|
+
* treatment as the link call itself: a rate limit or network blip on the
|
|
170
|
+
* comment call is just as cosmetic a failure as one on the link call, so
|
|
171
|
+
* it is caught and logged here rather than left to propagate out of
|
|
172
|
+
* `publish()` (`refine.ts`), which awaits this unguarded.
|
|
173
|
+
*
|
|
174
|
+
* `this.linkType` MUST name a SYMMETRIC link type ("Relates" and its
|
|
175
|
+
* project-renamed equivalents) — `inwardIssue`/`outwardIssue` above are
|
|
176
|
+
* fixed (the published root is always inward, the spec always outward)
|
|
177
|
+
* and not independently configurable, which is fine for a symmetric type
|
|
178
|
+
* (Jira does not surface the direction differently in the UI) but WRONG
|
|
179
|
+
* for a directional one (e.g. "blocks"/"is blocked by"): configuring
|
|
180
|
+
* `watch.jira.link_type` to a directional type would silently assert the
|
|
181
|
+
* opposite relationship from the one intended. See that field's own doc
|
|
182
|
+
* comment (`data_types.ts`) — direction is not exposed as a separate knob
|
|
183
|
+
* on purpose, to avoid a second config field only meaningful alongside a
|
|
184
|
+
* link type most projects never change from the "Relates" default.
|
|
185
|
+
*/
|
|
186
|
+
linkToSpec(specId: string, issue: Issue): Promise<void>;
|
|
137
187
|
/** The read-back half of `linkChild` — same JQL-in-body pattern as `searchByLabel`, since a GET with query params silently returns nothing on this endpoint (see the module comment). What makes container roll-up (`rollUp` in `watch.ts`) work on Jira too. */
|
|
138
188
|
listChildren(parent: Issue): Promise<Issue[]>;
|
|
139
189
|
/**
|
|
@@ -56,8 +56,12 @@ export class JiraProvider {
|
|
|
56
56
|
apiToken;
|
|
57
57
|
issueTypes;
|
|
58
58
|
statusMap;
|
|
59
|
+
linkType;
|
|
59
60
|
constructor(baseUrl, // e.g. "https://your-domain.atlassian.net", no trailing slash
|
|
60
|
-
projectKey, labelPrefix, email, apiToken, issueTypes, statusMap = {}
|
|
61
|
+
projectKey, labelPrefix, email, apiToken, issueTypes, statusMap = {},
|
|
62
|
+
// See `WatchJiraConfigSchema.link_type`'s own doc comment (`data_types.ts`)
|
|
63
|
+
// — the issue-link `type` name `linkToSpec` below creates.
|
|
64
|
+
linkType = "Relates") {
|
|
61
65
|
this.baseUrl = baseUrl;
|
|
62
66
|
this.projectKey = projectKey;
|
|
63
67
|
this.labelPrefix = labelPrefix;
|
|
@@ -65,6 +69,7 @@ export class JiraProvider {
|
|
|
65
69
|
this.apiToken = apiToken;
|
|
66
70
|
this.issueTypes = issueTypes;
|
|
67
71
|
this.statusMap = statusMap;
|
|
72
|
+
this.linkType = linkType;
|
|
68
73
|
}
|
|
69
74
|
authHeader() {
|
|
70
75
|
return `Basic ${Buffer.from(`${this.email}:${this.apiToken}`).toString("base64")}`;
|
|
@@ -185,6 +190,69 @@ export class JiraProvider {
|
|
|
185
190
|
async linkChild(parent, child) {
|
|
186
191
|
await this.jira(`/rest/api/3/issue/${child.id}`, { method: "PUT", body: JSON.stringify({ fields: { parent: { key: parent.id } } }) });
|
|
187
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* `IssueAuthoringProvider.linkToSpec` — see its own doc comment
|
|
195
|
+
* (`issues/provider.ts`) for why this is a plain Jira "issue link"
|
|
196
|
+
* (`/rest/api/3/issueLink`) rather than `linkChild`'s hierarchy `parent`
|
|
197
|
+
* field: the spec's issue type (Story by default) frequently cannot
|
|
198
|
+
* legally PARENT a root node's type under Jira's issue-type hierarchy,
|
|
199
|
+
* while a generic issue link has no such restriction.
|
|
200
|
+
*
|
|
201
|
+
* `inwardIssue`/`outwardIssue` direction is arbitrary for a symmetric
|
|
202
|
+
* type like "Relates" — Jira does not surface it differently in the UI —
|
|
203
|
+
* so `issue` (the freshly published root) is the inward side and the
|
|
204
|
+
* spec is the outward side, consistently.
|
|
205
|
+
*
|
|
206
|
+
* BEST-EFFORT: this project's Jira instance may not have `linkType`
|
|
207
|
+
* enabled/named exactly this way (a renamed or removed link type, a
|
|
208
|
+
* permission scheme that disallows issue links for this project, ...) —
|
|
209
|
+
* a failure here must never fail the whole publish over what is, at
|
|
210
|
+
* bottom, a cosmetic cross-reference. Falls back to a plain comment
|
|
211
|
+
* naming the spec, so the relationship is visible SOMEWHERE even when
|
|
212
|
+
* the link API itself is unavailable.
|
|
213
|
+
*
|
|
214
|
+
* The fallback comment gets the SAME "never fail publish() over this"
|
|
215
|
+
* treatment as the link call itself: a rate limit or network blip on the
|
|
216
|
+
* comment call is just as cosmetic a failure as one on the link call, so
|
|
217
|
+
* it is caught and logged here rather than left to propagate out of
|
|
218
|
+
* `publish()` (`refine.ts`), which awaits this unguarded.
|
|
219
|
+
*
|
|
220
|
+
* `this.linkType` MUST name a SYMMETRIC link type ("Relates" and its
|
|
221
|
+
* project-renamed equivalents) — `inwardIssue`/`outwardIssue` above are
|
|
222
|
+
* fixed (the published root is always inward, the spec always outward)
|
|
223
|
+
* and not independently configurable, which is fine for a symmetric type
|
|
224
|
+
* (Jira does not surface the direction differently in the UI) but WRONG
|
|
225
|
+
* for a directional one (e.g. "blocks"/"is blocked by"): configuring
|
|
226
|
+
* `watch.jira.link_type` to a directional type would silently assert the
|
|
227
|
+
* opposite relationship from the one intended. See that field's own doc
|
|
228
|
+
* comment (`data_types.ts`) — direction is not exposed as a separate knob
|
|
229
|
+
* on purpose, to avoid a second config field only meaningful alongside a
|
|
230
|
+
* link type most projects never change from the "Relates" default.
|
|
231
|
+
*/
|
|
232
|
+
async linkToSpec(specId, issue) {
|
|
233
|
+
try {
|
|
234
|
+
await this.jira("/rest/api/3/issueLink", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
type: { name: this.linkType },
|
|
238
|
+
inwardIssue: { key: issue.id },
|
|
239
|
+
outwardIssue: { key: specId },
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (linkError) {
|
|
244
|
+
try {
|
|
245
|
+
await this.comment(issue, `Refined from ${specId}.`);
|
|
246
|
+
}
|
|
247
|
+
catch (commentError) {
|
|
248
|
+
// Both the issue-link API and the comment fallback failed — the
|
|
249
|
+
// spec/root relationship is not recorded ANYWHERE on Jira this run,
|
|
250
|
+
// but that is still a cosmetic loss, not a reason to fail the whole
|
|
251
|
+
// publish (see this method's own doc comment).
|
|
252
|
+
console.error(`spf watch: ${issue.id} — could not link to spec ${specId} (${linkError instanceof Error ? linkError.message : String(linkError)}) and the fallback comment also failed — ${commentError instanceof Error ? commentError.message : String(commentError)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
188
256
|
/** The read-back half of `linkChild` — same JQL-in-body pattern as `searchByLabel`, since a GET with query params silently returns nothing on this endpoint (see the module comment). What makes container roll-up (`rollUp` in `watch.ts`) work on Jira too. */
|
|
189
257
|
async listChildren(parent) {
|
|
190
258
|
const jql = `parent = ${JSON.stringify(parent.id)}`;
|
|
@@ -311,6 +311,29 @@ export interface IssueAuthoringProvider {
|
|
|
311
311
|
* roll-up, refine cannot function without authoring at all.
|
|
312
312
|
*/
|
|
313
313
|
listChildren(parent: Issue): Promise<Issue[]>;
|
|
314
|
+
/**
|
|
315
|
+
* Best-effort: relate a freshly published tree's ROOT issue back to the
|
|
316
|
+
* spec issue (`specId`) it was refined FROM — `core/refine.ts`'s
|
|
317
|
+
* `publish()` calls this once per root node (a node with no `parent` of
|
|
318
|
+
* its own within the refined tree) when it was given a `specIssueId`.
|
|
319
|
+
*
|
|
320
|
+
* Deliberately NOT `linkChild`: that method sets the tracker's
|
|
321
|
+
* HIERARCHY field (Jira's `parent`, GitHub's sub-issues API), and a
|
|
322
|
+
* spec's own issue type (Story by default — `JiraIssueTypeMapSchema.spec`)
|
|
323
|
+
* frequently cannot legally PARENT a root node's type in Jira's
|
|
324
|
+
* issue-type hierarchy — see `JiraProvider`'s own doc comment on
|
|
325
|
+
* `publishSpecs()` for the same constraint. `linkToSpec` uses a plain,
|
|
326
|
+
* symmetric issue-to-issue reference instead (Jira's generic "issue
|
|
327
|
+
* link"), which has no such hierarchy restriction.
|
|
328
|
+
*
|
|
329
|
+
* OPTIONAL, not every tracker needs one: GitHub already gets a native,
|
|
330
|
+
* visible cross-reference for free the moment `renderBody`'s "## Parent"
|
|
331
|
+
* section renders a plain "#N" in the body — GitHub auto-links same-repo
|
|
332
|
+
* issue mentions into a real timeline "referenced this issue" event, no
|
|
333
|
+
* API call required. Jira does not do this for plain text, which is why
|
|
334
|
+
* `JiraProvider` implements this and `GitHubProvider` does not.
|
|
335
|
+
*/
|
|
336
|
+
linkToSpec?(specId: string, issue: Issue): Promise<void>;
|
|
314
337
|
}
|
|
315
338
|
/**
|
|
316
339
|
* Structural, not nominal: checks for the three methods rather than
|
package/dist/core/loop.d.ts
CHANGED
|
@@ -106,7 +106,35 @@ export interface LedgerAttempt {
|
|
|
106
106
|
error: string | null;
|
|
107
107
|
/** HEAD's short sha after a successful (exit 0) iteration — unchanged from the previous attempt's if this iteration committed nothing new, which is exactly what `isStuck` below looks for. `null` only for an errored or non-accepted (nonzero exit) attempt, where nothing was checked out to read. */
|
|
108
108
|
commit_sha: string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Kept for backward compatibility with ledger rows written before
|
|
111
|
+
* `billable_tokens` (below) existed — on a row this old, `tokens` is the
|
|
112
|
+
* DISPLAY total (`sessions.total_tokens`, cache reads included), the only
|
|
113
|
+
* figure that era ever recorded. On every row written since, this field
|
|
114
|
+
* and `billable_tokens` are set to the SAME (billable) value; they only
|
|
115
|
+
* ever diverge across that one-time boundary, never within a single
|
|
116
|
+
* write. `cumulativeSpend` is what actually reads either — see its own
|
|
117
|
+
* doc comment for the precedence.
|
|
118
|
+
*/
|
|
109
119
|
tokens: number;
|
|
120
|
+
/**
|
|
121
|
+
* The BILLABLE figure (input + cache-write + output) — what
|
|
122
|
+
* `--max-tokens`/`overCumulativeBudget` is SUPPOSED to compare against,
|
|
123
|
+
* same metric `defaults.max_run_tokens` uses within one chain run (see
|
|
124
|
+
* `UsageBreakdown.billable_tokens`'s doc comment, `data_types.ts`). NOT
|
|
125
|
+
* the display total (`sessions.total_tokens`, cache reads included) —
|
|
126
|
+
* `cli/commands/loop.ts`'s readback reads `session.billable_tokens`,
|
|
127
|
+
* falling back to `total_tokens` only for a SESSION row that predates
|
|
128
|
+
* that sqlite column being populated.
|
|
129
|
+
*
|
|
130
|
+
* `undefined` on any LEDGER row written before this field existed — a
|
|
131
|
+
* genuinely old-format row, not a free ($0) iteration. `cumulativeSpend`
|
|
132
|
+
* falls back to `tokens` for exactly that row (the only figure it has,
|
|
133
|
+
* a display total it cannot retroactively convert), and
|
|
134
|
+
* `hasLegacyTokenRows` is what a caller uses to warn once that a ceiling
|
|
135
|
+
* check is now mixing metrics across the ledger's own history.
|
|
136
|
+
*/
|
|
137
|
+
billable_tokens?: number;
|
|
110
138
|
cost: number;
|
|
111
139
|
failures: string[];
|
|
112
140
|
started_at: string;
|
|
@@ -151,13 +179,22 @@ export declare function newLedger(input: {
|
|
|
151
179
|
export declare function resolveGoalId(explicit: string | undefined): string;
|
|
152
180
|
export interface CumulativeBudget {
|
|
153
181
|
maxCost?: number;
|
|
182
|
+
/** `--max-tokens` — checked against BILLABLE tokens (see `LedgerAttempt.billable_tokens`'s own doc comment), the same metric `defaults.max_run_tokens` checks per-call inside one chain run. */
|
|
154
183
|
maxTokens?: number;
|
|
155
184
|
}
|
|
156
185
|
export declare function cumulativeSpend(attempts: LedgerAttempt[]): {
|
|
157
186
|
cost: number;
|
|
158
187
|
tokens: number;
|
|
159
188
|
};
|
|
160
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* True when the ledger holds at least one attempt written before
|
|
191
|
+
* `billable_tokens` existed — `cumulativeSpend` is then silently mixing a
|
|
192
|
+
* display-total figure (that row's `tokens`) into a sum whose newer rows
|
|
193
|
+
* are billable. Used to print a one-time operator warning rather than let
|
|
194
|
+
* that mismatch pass unremarked; see `runLoop`'s call site.
|
|
195
|
+
*/
|
|
196
|
+
export declare function hasLegacyTokenRows(attempts: LedgerAttempt[]): boolean;
|
|
197
|
+
/** Whether the goal-scoped ceiling is already exhausted going into the NEXT iteration — a stronger, ledger-wide check than any single iteration's own budget. `spend.tokens` must already be BILLABLE tokens (see `LedgerAttempt.tokens`) — this function just compares, it does not know which metric it was handed. */
|
|
161
198
|
export declare function overCumulativeBudget(budget: CumulativeBudget, spend: {
|
|
162
199
|
cost: number;
|
|
163
200
|
tokens: number;
|
|
@@ -185,6 +222,7 @@ export interface IterationResult {
|
|
|
185
222
|
error: string | null;
|
|
186
223
|
/** `null` when nothing was committed this iteration (a no-op, or a throw before any commit). */
|
|
187
224
|
commit_sha: string | null;
|
|
225
|
+
/** BILLABLE tokens — see `LedgerAttempt.tokens`'s own doc comment; `runIteration` (`cli/commands/loop.ts`) reads this straight from `LedgerAttempt`, so the two must stay the same metric. */
|
|
188
226
|
tokens: number;
|
|
189
227
|
cost: number;
|
|
190
228
|
/** `null` when the iteration errored or exited non-zero — the stop check only ever runs against a chain that accepted its own work, same as `fixLoop` only re-verifies after a phase that didn't already throw. */
|
package/dist/core/loop.js
CHANGED
|
@@ -132,10 +132,30 @@ export function newLedger(input) {
|
|
|
132
132
|
export function resolveGoalId(explicit) {
|
|
133
133
|
return explicit ?? newId(8);
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Per-attempt token figure for budget purposes: `billable_tokens` where the
|
|
137
|
+
* row has it, else `tokens` (an old-format row, predating that field — see
|
|
138
|
+
* `LedgerAttempt.billable_tokens`'s own doc comment). Never the other way
|
|
139
|
+
* around: a new row's `tokens` happens to equal its `billable_tokens` today,
|
|
140
|
+
* but `billable_tokens` is the one this function trusts on purpose.
|
|
141
|
+
*/
|
|
142
|
+
function tokensForBudget(a) {
|
|
143
|
+
return a.billable_tokens ?? a.tokens;
|
|
144
|
+
}
|
|
135
145
|
export function cumulativeSpend(attempts) {
|
|
136
|
-
return attempts.reduce((acc, a) => ({ cost: acc.cost + a.cost, tokens: acc.tokens + a
|
|
146
|
+
return attempts.reduce((acc, a) => ({ cost: acc.cost + a.cost, tokens: acc.tokens + tokensForBudget(a) }), { cost: 0, tokens: 0 });
|
|
137
147
|
}
|
|
138
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* True when the ledger holds at least one attempt written before
|
|
150
|
+
* `billable_tokens` existed — `cumulativeSpend` is then silently mixing a
|
|
151
|
+
* display-total figure (that row's `tokens`) into a sum whose newer rows
|
|
152
|
+
* are billable. Used to print a one-time operator warning rather than let
|
|
153
|
+
* that mismatch pass unremarked; see `runLoop`'s call site.
|
|
154
|
+
*/
|
|
155
|
+
export function hasLegacyTokenRows(attempts) {
|
|
156
|
+
return attempts.some((a) => a.billable_tokens === undefined);
|
|
157
|
+
}
|
|
158
|
+
/** Whether the goal-scoped ceiling is already exhausted going into the NEXT iteration — a stronger, ledger-wide check than any single iteration's own budget. `spend.tokens` must already be BILLABLE tokens (see `LedgerAttempt.tokens`) — this function just compares, it does not know which metric it was handed. */
|
|
139
159
|
export function overCumulativeBudget(budget, spend) {
|
|
140
160
|
if (budget.maxCost !== undefined && spend.cost >= budget.maxCost)
|
|
141
161
|
return true;
|
|
@@ -207,6 +227,13 @@ export async function runLoop(deps) {
|
|
|
207
227
|
if (existing) {
|
|
208
228
|
deps.log(`loop: resuming goal ${deps.goalId} — ${existing.attempts.length} attempt(s) already recorded`);
|
|
209
229
|
}
|
|
230
|
+
// One-time info line, not a per-iteration one: legacy rows already IN the
|
|
231
|
+
// ledger (from a resumed goal) do not change count as this run proceeds,
|
|
232
|
+
// so there is nothing to re-warn about after the first check.
|
|
233
|
+
if (deps.budget.maxTokens !== undefined && hasLegacyTokenRows(ledger.attempts)) {
|
|
234
|
+
deps.log(`loop: goal ${deps.goalId} — ledger has attempt(s) recorded before billable-token tracking; ` +
|
|
235
|
+
"--max-tokens falls back to their display-total token count for those rows (see LedgerAttempt.billable_tokens)");
|
|
236
|
+
}
|
|
210
237
|
let lastVerdict = ledger.attempts.length > 0 ? { passed: false, failures: ledger.attempts[ledger.attempts.length - 1].failures, artifacts: [] } : null;
|
|
211
238
|
for (let i = ledger.attempts.length + 1; i <= deps.max; i++) {
|
|
212
239
|
if (overCumulativeBudget(deps.budget, cumulativeSpend(ledger.attempts))) {
|
|
@@ -244,7 +271,11 @@ export async function runLoop(deps) {
|
|
|
244
271
|
exit_code: result.exit_code,
|
|
245
272
|
error: result.error,
|
|
246
273
|
commit_sha: result.commit_sha,
|
|
274
|
+
// Every row written from here on sets both to the same (billable)
|
|
275
|
+
// value — see `LedgerAttempt.tokens`/`billable_tokens`'s doc comments
|
|
276
|
+
// for why they only ever diverge on an old-format row.
|
|
247
277
|
tokens: result.tokens,
|
|
278
|
+
billable_tokens: result.tokens,
|
|
248
279
|
cost: result.cost,
|
|
249
280
|
failures,
|
|
250
281
|
started_at: startedAt,
|
|
@@ -42,28 +42,111 @@
|
|
|
42
42
|
* cost nothing at runtime either way.
|
|
43
43
|
*/
|
|
44
44
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
45
|
+
/**
|
|
46
|
+
* `OLLAMA_API_KEY`, trimmed, when the operator has set one — e.g. the
|
|
47
|
+
* Briefs gateway (Envoy AI Gateway) enforces a per-client bearer and 401s
|
|
48
|
+
* the dummy key `DUMMY_API_KEY` was designed for a bare local server that
|
|
49
|
+
* checks nothing. Falls back to the dummy exactly as before when unset, so
|
|
50
|
+
* THIS function's own return value — the resolved `Authorization` bearer —
|
|
51
|
+
* is byte-identical to pre-gateway behavior for a bare local Ollama server
|
|
52
|
+
* (the common case this module was built for). MINOR-H: that is narrower
|
|
53
|
+
* than "the whole request is unchanged" — it is not, even with
|
|
54
|
+
* `OLLAMA_API_KEY` unset: a `traceparent` (and, once `agent_flue.ts` has an
|
|
55
|
+
* adw_id/agent_name to give it, `x-correlation-id`/`x-spf-agent`) is ALWAYS
|
|
56
|
+
* sent now, gateway or no gateway (see the "Gateway headers" section below).
|
|
57
|
+
* A bare local Ollama server ignores headers it doesn't recognize, so this
|
|
58
|
+
* is harmless — just not byte-identical. Read fresh inside `resolve()` (see
|
|
59
|
+
* its call site below) — never cached — so a key exported mid-process (or
|
|
60
|
+
* changed) takes effect on the very next dispatch with no re-registration.
|
|
61
|
+
*
|
|
62
|
+
* MINOR 3: exported so `doctor.ts`'s `OLLAMA_BASE_URL reachability` probe
|
|
63
|
+
* calls this SAME function rather than reading `process.env.OLLAMA_API_KEY`
|
|
64
|
+
* raw — a prior version of that probe sent NO `Authorization` header at all
|
|
65
|
+
* when the env var was unset, which diverges from what a real dispatch
|
|
66
|
+
* sends (the dummy bearer below, always). Against a gateway that rejects a
|
|
67
|
+
* request with no `Authorization` header at all differently than one with a
|
|
68
|
+
* wrong/dummy bearer, that divergence could make doctor report reachable
|
|
69
|
+
* when a real dispatch would 401, or vice versa. Calling `ollamaApiKey()` in
|
|
70
|
+
* both places means doctor's probe and a real dispatch send byte-identical
|
|
71
|
+
* bearers for the same env state.
|
|
72
|
+
*/
|
|
73
|
+
export declare function ollamaApiKey(): string;
|
|
74
|
+
/** SPF-side identity for one LLM call, as far as `registerOllamaModel`'s caller can supply it. Both fields optional — an absent one simply omits its header. */
|
|
75
|
+
export interface GatewayCallContext {
|
|
76
|
+
/** The run's adw_id — sent as `x-correlation-id` so the gateway groups this run's calls. */
|
|
77
|
+
adwId?: string;
|
|
78
|
+
/** The SPF agent name — sent as `x-spf-agent`. */
|
|
79
|
+
agentName?: string;
|
|
80
|
+
}
|
|
81
|
+
/** 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). */
|
|
82
|
+
export declare const X_REQUEST_ID_HEADER = "x-request-id";
|
|
83
|
+
/**
|
|
84
|
+
* A fresh W3C `traceparent` for one outbound call — used only on the NOT
|
|
85
|
+
* INSTALLED path (see the section above); when propagation IS installed,
|
|
86
|
+
* `resolve()` does not call this at all, relying entirely on the
|
|
87
|
+
* instrumentation's own per-request injection instead (this is what fixed
|
|
88
|
+
* BLOCKER A/MAJOR-C: this function used to be called unconditionally, and
|
|
89
|
+
* on the installed path it silently reused the one still-open span's
|
|
90
|
+
* traceparent for every call inside that span, which is both a duplicate
|
|
91
|
+
* header AND not actually fresh per call).
|
|
92
|
+
*
|
|
93
|
+
* Reuses the active OTel span context when one is installed and current —
|
|
94
|
+
* the same trace this call's other telemetry already belongs to — falling
|
|
95
|
+
* back to a brand-new random trace/span id pair when there is none (no span
|
|
96
|
+
* active at this exact point, e.g. a stray call before any span opened), so
|
|
97
|
+
* the gateway still gets a well-formed, per-call-unique traceparent either
|
|
98
|
+
* way. Never throws; `isSpanContextValid` is the same guard
|
|
99
|
+
* `otel_propagation.ts`'s own propagators use.
|
|
100
|
+
*/
|
|
101
|
+
export declare function freshTraceparent(): string;
|
|
45
102
|
/** Exported so `doctor.ts`'s reachability probe agrees with what a real dispatch resolves to — see its call site for why a `??`/`||` mismatch here matters. */
|
|
46
103
|
export declare function ollamaBaseUrl(): string;
|
|
47
104
|
/**
|
|
48
105
|
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
49
106
|
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
50
|
-
* id ever registered this process.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
107
|
+
* id ever registered this process. NOT idempotent w.r.t. `ctx` (see MAJOR-D
|
|
108
|
+
* below) — every call re-runs the union re-registration (dynamic imports
|
|
109
|
+
* are cheap after the first, and `setProvider()` is a cheap in-memory
|
|
110
|
+
* upsert), so this id's `Model.headers` always reflect the MOST RECENT
|
|
111
|
+
* `ctx` this function was called with, not just the first. A concurrent
|
|
112
|
+
* call for the SAME id joins the in-flight registration rather than running
|
|
113
|
+
* a second one in parallel (see `inflight`'s doc); `registeredIds` itself
|
|
114
|
+
* is only ever updated AFTER `setProvider()` succeeds, so a failed attempt
|
|
115
|
+
* (a bad install, a bundler that can't resolve the deep `.lazy` subpath, a
|
|
116
|
+
* future validation error) leaves the id unregistered and eligible for a
|
|
117
|
+
* real retry — not permanently and misleadingly marked "done" while
|
|
118
|
+
* nothing is actually registered.
|
|
59
119
|
*
|
|
60
120
|
* Must complete before the FIRST Flue dispatch that names this model
|
|
61
121
|
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
62
|
-
* but is equally safe to call again later with a new id
|
|
63
|
-
*
|
|
64
|
-
* added without orphaning the first (see the `registeredIds` doc
|
|
122
|
+
* but is equally safe to call again later with a new id, or the SAME id
|
|
123
|
+
* again, mid-process — a new id's union re-registration is how a second
|
|
124
|
+
* model gets added without orphaning the first (see the `registeredIds` doc
|
|
125
|
+
* above); a repeat of the SAME id is how MAJOR-D below is fixed.
|
|
126
|
+
*
|
|
127
|
+
* MAJOR-D (fixed): `ctx`, when given AND `isFluePropagationInstalled()` is
|
|
128
|
+
* false (see the "Gateway headers" section above — when it's true, these
|
|
129
|
+
* two headers come from the per-request `GatewayHeadersPropagator`
|
|
130
|
+
* instead), is stamped onto this id's `Model.headers` as
|
|
131
|
+
* `x-correlation-id`/`x-spf-agent`. A PRIOR version of this function
|
|
132
|
+
* returned immediately for an already-registered id (a false comment
|
|
133
|
+
* claimed "one spf process runs one adw_id for its whole lifetime" to
|
|
134
|
+
* justify this) — which meant every later agent/adw_id sharing a model id
|
|
135
|
+
* within one process (spf `loop`/`fanout`/`watch`, which run many adw_ids
|
|
136
|
+
* in ONE process, `fanout` concurrently) silently kept the FIRST
|
|
137
|
+
* registration's headers forever. Re-running the full registration on every
|
|
138
|
+
* call, unconditionally, fixes that for every case except one, which
|
|
139
|
+
* remains and is not silently swallowed: two flue agents dispatching
|
|
140
|
+
* CONCURRENTLY (not sequentially) to the SAME `ollama/<id>` model id race on
|
|
141
|
+
* `registrationContext`/`setProvider()` — whichever registration's
|
|
142
|
+
* `setProvider()` call lands last wins the headers BOTH calls' subsequent
|
|
143
|
+
* dispatches see, until the next registration for that id. This is a
|
|
144
|
+
* `fanout` concurrency > 1 scenario specifically (two DIFFERENT agents,
|
|
145
|
+
* same process, same model id, truly overlapping registrations) — a
|
|
146
|
+
* sequential loop/watch never hits it, since each call's `await` completes
|
|
147
|
+
* before the next one starts.
|
|
65
148
|
*/
|
|
66
|
-
export declare function registerOllamaModel(modelId: string): Promise<void>;
|
|
149
|
+
export declare function registerOllamaModel(modelId: string, ctx?: GatewayCallContext): Promise<void>;
|
|
67
150
|
/** Test-only: the most recently constructed provider object (see `lastProvider`'s doc). */
|
|
68
151
|
export declare function providerForTest(): Provider<"openai-completions"> | undefined;
|
|
69
152
|
/** Test-only: forgets accumulated ids so test files don't leak into each other. Does not touch Flue's own registry — pair with `resetModelsForTests()` from `@flue/runtime/internal`. */
|