@gr8ful/spf 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -4
- package/assets/defaults/spf.config.yaml +6 -0
- package/assets/prompts/reviewer/system.md +1 -1
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/cookbooks/authoring_chains.md +90 -7
- package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
- package/assets/skill/cookbooks/roster.md +15 -4
- package/assets/skill/cookbooks/spf_overview.md +1 -0
- package/assets/skill/references/config.md +69 -4
- package/assets/skill/references/observability.md +11 -2
- package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
- package/assets/templates/ts.spf.config.yaml +5 -0
- package/dist/chains/context.d.ts +30 -0
- package/dist/chains/index.d.ts +94 -10
- package/dist/chains/index.js +70 -5
- package/dist/chains/repo_chains.d.ts +139 -0
- package/dist/chains/repo_chains.js +428 -0
- package/dist/chains/simple_sdlc.d.ts +74 -1
- package/dist/chains/simple_sdlc.js +134 -4
- package/dist/chains/steps.d.ts +215 -20
- package/dist/chains/steps.js +429 -61
- package/dist/cli/ask.d.ts +14 -1
- package/dist/cli/ask.js +32 -2
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +319 -11
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +78 -1
- package/dist/cli/commands/list.js +42 -5
- package/dist/cli/commands/run.js +25 -2
- package/dist/cli/commands/watch.d.ts +18 -0
- package/dist/cli/commands/watch.js +147 -10
- package/dist/cli/index.js +60 -3
- package/dist/cli/interview.js +65 -10
- package/dist/core/agent_cc.d.ts +40 -1
- package/dist/core/agent_cc.js +51 -4
- package/dist/core/agent_flue.js +28 -4
- package/dist/core/agents.d.ts +8 -0
- package/dist/core/agents.js +43 -3
- package/dist/core/data_types.d.ts +104 -4
- package/dist/core/data_types.js +99 -2
- package/dist/core/git_helper.d.ts +29 -0
- package/dist/core/git_helper.js +41 -1
- package/dist/core/ollama_provider.d.ts +70 -0
- package/dist/core/ollama_provider.js +208 -0
- package/dist/core/otel.d.ts +352 -0
- package/dist/core/otel.js +793 -0
- package/dist/core/providers.js +4 -0
- package/dist/core/refine.js +11 -3
- package/dist/core/session.js +39 -2
- package/dist/core/tracer.d.ts +31 -2
- package/dist/core/tracer.js +69 -11
- package/dist/core/watch.d.ts +11 -0
- package/dist/core/watch.js +17 -2
- package/dist/test/chains.test.js +8 -3
- package/dist/test/data_types.test.js +140 -2
- package/dist/test/git_helper.test.d.ts +1 -0
- package/dist/test/git_helper.test.js +59 -0
- package/dist/test/hermetic_git.d.ts +1 -0
- package/dist/test/hermetic_git.js +22 -0
- package/dist/test/init_command.test.d.ts +14 -1
- package/dist/test/init_command.test.js +54 -1
- package/dist/test/interview.test.d.ts +15 -1
- package/dist/test/interview.test.js +127 -0
- package/dist/test/ollama_provider.test.d.ts +1 -0
- package/dist/test/ollama_provider.test.js +103 -0
- package/dist/test/otel.test.d.ts +26 -0
- package/dist/test/otel.test.js +512 -0
- package/dist/test/refine.test.js +64 -1
- package/dist/test/repo_chains.test.d.ts +21 -0
- package/dist/test/repo_chains.test.js +416 -0
- package/dist/test/signoff.test.d.ts +1 -0
- package/dist/test/signoff.test.js +329 -0
- package/dist/test/ui_server.test.d.ts +7 -1
- package/dist/test/ui_server.test.js +1 -0
- package/dist/test/watch.test.js +124 -1
- package/package.json +5 -5
package/dist/core/git_helper.js
CHANGED
|
@@ -28,13 +28,53 @@ export function isRepoAt(cwd) {
|
|
|
28
28
|
* not inside one — ADWs run fine in a non-git dir; only a commit phase
|
|
29
29
|
* requires a repo. Always absolute, so it is safe to hand to a subprocess
|
|
30
30
|
* regardless of where the ADW was launched from.
|
|
31
|
+
*
|
|
32
|
+
* `isRepoAt` only proves `git rev-parse --git-dir` succeeds, which is also
|
|
33
|
+
* true inside a bare repo and inside a `.git/` directory itself — neither
|
|
34
|
+
* has a work tree, so `--show-toplevel` fails there even though `isRepoAt`
|
|
35
|
+
* said yes. That failure is not a bug to propagate: there is still an
|
|
36
|
+
* honest answer (`cwd` itself), so it falls back rather than throwing —
|
|
37
|
+
* this function's whole contract is "never throws, always returns some root."
|
|
31
38
|
*/
|
|
32
39
|
export function findRepoRoot(cwd) {
|
|
33
40
|
if (isRepoAt(cwd)) {
|
|
34
|
-
|
|
41
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" });
|
|
42
|
+
if (result.status === 0)
|
|
43
|
+
return path.resolve(result.stdout.trim());
|
|
35
44
|
}
|
|
36
45
|
return path.resolve(cwd);
|
|
37
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* `git config user.name`/`user.email` at `repoRoot` — `undefined` (never a
|
|
49
|
+
* fallback literal) when either is unset, so a caller can tell "no identity
|
|
50
|
+
* configured" from "identity is the empty string."
|
|
51
|
+
*
|
|
52
|
+
* Deliberately NOT `utils.engineerName()`: that helper tries the
|
|
53
|
+
* `ENGINEER_NAME` env var (spoofable by an operator), then `git config
|
|
54
|
+
* user.name`, then `$USER`/`$USERNAME`, and finally falls back to the
|
|
55
|
+
* literal string `"engineer"` when nothing is set — fine for a phase's
|
|
56
|
+
* display `owner`, but a `Signed-off-by:` trailer is a git attestation, and
|
|
57
|
+
* this is the one thing on this branch that ends up in one (see
|
|
58
|
+
* `chains/simple_sdlc.ts`'s `decideSignoff` and `chains/steps.ts`'s
|
|
59
|
+
* `commitEnvelope`). A trailer needs the identity git itself would use for
|
|
60
|
+
* the commit — `undefined` here means the caller records the sign-off
|
|
61
|
+
* decision in the trace anyway and skips the trailer with a logged note,
|
|
62
|
+
* rather than inventing a name for it.
|
|
63
|
+
*/
|
|
64
|
+
export function committerIdentity(repoRoot) {
|
|
65
|
+
const name = gitConfigValue(repoRoot, "user.name");
|
|
66
|
+
const email = gitConfigValue(repoRoot, "user.email");
|
|
67
|
+
if (!name || !email)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { name, email };
|
|
70
|
+
}
|
|
71
|
+
/** `""` on anything short of a clean, non-empty value — unset, unreadable, or blank all read the same to a caller that only wants "do we have one?" */
|
|
72
|
+
function gitConfigValue(repoRoot, key) {
|
|
73
|
+
const result = spawnSync("git", ["config", "--get", key], { cwd: repoRoot, encoding: "utf-8" });
|
|
74
|
+
if (result.status !== 0)
|
|
75
|
+
return "";
|
|
76
|
+
return result.stdout.trim();
|
|
77
|
+
}
|
|
38
78
|
/** Every operation this returns is bound to `repoRoot` — never `process.cwd()`. */
|
|
39
79
|
export function makeGit(repoRoot) {
|
|
40
80
|
const run = (args) => git(args, repoRoot);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama registration for the Flue backend (agent_flue.ts).
|
|
3
|
+
*
|
|
4
|
+
* Neither pi-ai nor Flue ship a built-in "ollama" provider — Ollama is
|
|
5
|
+
* reached through the OpenAI-compatible `/v1/chat/completions` surface it
|
|
6
|
+
* serves locally, registered the same way any self-hosted OpenAI-compatible
|
|
7
|
+
* endpoint would be: `createProvider()` + Flue's `setProvider()`. Three
|
|
8
|
+
* constraints below came out of a live spike (raw logs under
|
|
9
|
+
* scratchpad/ollama-spike) and would look like accidental complexity to a
|
|
10
|
+
* future maintainer without this note, so each is called out where it bites.
|
|
11
|
+
*
|
|
12
|
+
* EXACT VERSION PIN — see package.json's `@earendil-works/pi-ai: "0.83.0"`
|
|
13
|
+
* (no caret). `@flue/runtime@2.0.3` itself depends on `^0.83.0`; pinning our
|
|
14
|
+
* own dependency to the exact same version lets npm dedupe both into ONE
|
|
15
|
+
* physical copy of pi-ai in node_modules. A newer 0.83.x/0.84.x would still
|
|
16
|
+
* satisfy flue's range, but npm would then keep two-or-three separate copies
|
|
17
|
+
* side by side — and the `Provider`/`Model` values this file hands to
|
|
18
|
+
* `setProvider()` must be instances `@flue/runtime`'s OWN copy of pi-ai
|
|
19
|
+
* recognizes, or registration silently never reaches the registry flue's
|
|
20
|
+
* `resolveModel()` actually reads from. Verified live: with the exact pin,
|
|
21
|
+
* dedupe holds and registration is visible to flue's registry immediately.
|
|
22
|
+
*
|
|
23
|
+
* LAZY IMPORT — every symbol used here is loaded via dynamic `import()`
|
|
24
|
+
* inside `registerOllamaModel`, never at this module's top level. This is
|
|
25
|
+
* NOT a load-time saving, and the reasoning below is the measured truth, not
|
|
26
|
+
* the "avoid eagerly loading pi-ai's runtime for non-ollama runs" story that
|
|
27
|
+
* comment used to tell: `agent_flue.ts` already imports `@flue/runtime/node`
|
|
28
|
+
* unconditionally, and that import ALONE already pulls in pi-ai's full
|
|
29
|
+
* runtime (auth flows, every provider's model-catalog JSON, OAuth machinery)
|
|
30
|
+
* for every SPF run, ollama or not. Measured live: `await
|
|
31
|
+
* import("@flue/runtime/node")` costs 183ms / 38 pi-ai module-cache entries
|
|
32
|
+
* by itself; the subsequent `await import("@earendil-works/pi-ai")` costs
|
|
33
|
+
* 0ms / 0 additional entries, and `await
|
|
34
|
+
* import("@earendil-works/pi-ai/api/openai-completions.lazy")` costs 1ms / 0
|
|
35
|
+
* additional entries — pi-ai is already resident by the time either dynamic
|
|
36
|
+
* import here runs. The real reason to keep the dynamic form is narrower:
|
|
37
|
+
* it keeps this module's ollama-only symbols — especially the deep
|
|
38
|
+
* `api/openai-completions.lazy` subpath — off the module graph of anything
|
|
39
|
+
* that merely imports `agent_flue.ts` for `resolveModel()` (as `doctor.ts`
|
|
40
|
+
* and `interview.ts` both do) without ever dispatching an ollama call.
|
|
41
|
+
* `type` imports below are erased at compile time (verbatimModuleSyntax) and
|
|
42
|
+
* cost nothing at runtime either way.
|
|
43
|
+
*/
|
|
44
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
45
|
+
/** 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
|
+
export declare function ollamaBaseUrl(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
49
|
+
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
50
|
+
* id ever registered this process. Idempotent: a repeat of an already-seen
|
|
51
|
+
* id is a no-op — no re-registration, no re-import. A concurrent call for
|
|
52
|
+
* the SAME id joins the in-flight registration rather than returning early
|
|
53
|
+
* (see `inflight`'s doc); `registeredIds` itself is only ever updated AFTER
|
|
54
|
+
* `setProvider()` succeeds, so a failed attempt (a bad install, a bundler
|
|
55
|
+
* that can't resolve the deep `.lazy` subpath, a future validation error)
|
|
56
|
+
* leaves the id unregistered and eligible for a real retry — not
|
|
57
|
+
* permanently and misleadingly marked "done" while nothing is actually
|
|
58
|
+
* registered.
|
|
59
|
+
*
|
|
60
|
+
* Must complete before the FIRST Flue dispatch that names this model
|
|
61
|
+
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
62
|
+
* but is equally safe to call again later with a new id mid-process — that
|
|
63
|
+
* later call's union re-registration is exactly how a second model gets
|
|
64
|
+
* added without orphaning the first (see the `registeredIds` doc above).
|
|
65
|
+
*/
|
|
66
|
+
export declare function registerOllamaModel(modelId: string): Promise<void>;
|
|
67
|
+
/** Test-only: the most recently constructed provider object (see `lastProvider`'s doc). */
|
|
68
|
+
export declare function providerForTest(): Provider<"openai-completions"> | undefined;
|
|
69
|
+
/** 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`. */
|
|
70
|
+
export declare function resetOllamaRegistrationForTest(): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama registration for the Flue backend (agent_flue.ts).
|
|
3
|
+
*
|
|
4
|
+
* Neither pi-ai nor Flue ship a built-in "ollama" provider — Ollama is
|
|
5
|
+
* reached through the OpenAI-compatible `/v1/chat/completions` surface it
|
|
6
|
+
* serves locally, registered the same way any self-hosted OpenAI-compatible
|
|
7
|
+
* endpoint would be: `createProvider()` + Flue's `setProvider()`. Three
|
|
8
|
+
* constraints below came out of a live spike (raw logs under
|
|
9
|
+
* scratchpad/ollama-spike) and would look like accidental complexity to a
|
|
10
|
+
* future maintainer without this note, so each is called out where it bites.
|
|
11
|
+
*
|
|
12
|
+
* EXACT VERSION PIN — see package.json's `@earendil-works/pi-ai: "0.83.0"`
|
|
13
|
+
* (no caret). `@flue/runtime@2.0.3` itself depends on `^0.83.0`; pinning our
|
|
14
|
+
* own dependency to the exact same version lets npm dedupe both into ONE
|
|
15
|
+
* physical copy of pi-ai in node_modules. A newer 0.83.x/0.84.x would still
|
|
16
|
+
* satisfy flue's range, but npm would then keep two-or-three separate copies
|
|
17
|
+
* side by side — and the `Provider`/`Model` values this file hands to
|
|
18
|
+
* `setProvider()` must be instances `@flue/runtime`'s OWN copy of pi-ai
|
|
19
|
+
* recognizes, or registration silently never reaches the registry flue's
|
|
20
|
+
* `resolveModel()` actually reads from. Verified live: with the exact pin,
|
|
21
|
+
* dedupe holds and registration is visible to flue's registry immediately.
|
|
22
|
+
*
|
|
23
|
+
* LAZY IMPORT — every symbol used here is loaded via dynamic `import()`
|
|
24
|
+
* inside `registerOllamaModel`, never at this module's top level. This is
|
|
25
|
+
* NOT a load-time saving, and the reasoning below is the measured truth, not
|
|
26
|
+
* the "avoid eagerly loading pi-ai's runtime for non-ollama runs" story that
|
|
27
|
+
* comment used to tell: `agent_flue.ts` already imports `@flue/runtime/node`
|
|
28
|
+
* unconditionally, and that import ALONE already pulls in pi-ai's full
|
|
29
|
+
* runtime (auth flows, every provider's model-catalog JSON, OAuth machinery)
|
|
30
|
+
* for every SPF run, ollama or not. Measured live: `await
|
|
31
|
+
* import("@flue/runtime/node")` costs 183ms / 38 pi-ai module-cache entries
|
|
32
|
+
* by itself; the subsequent `await import("@earendil-works/pi-ai")` costs
|
|
33
|
+
* 0ms / 0 additional entries, and `await
|
|
34
|
+
* import("@earendil-works/pi-ai/api/openai-completions.lazy")` costs 1ms / 0
|
|
35
|
+
* additional entries — pi-ai is already resident by the time either dynamic
|
|
36
|
+
* import here runs. The real reason to keep the dynamic form is narrower:
|
|
37
|
+
* it keeps this module's ollama-only symbols — especially the deep
|
|
38
|
+
* `api/openai-completions.lazy` subpath — off the module graph of anything
|
|
39
|
+
* that merely imports `agent_flue.ts` for `resolveModel()` (as `doctor.ts`
|
|
40
|
+
* and `interview.ts` both do) without ever dispatching an ollama call.
|
|
41
|
+
* `type` imports below are erased at compile time (verbatimModuleSyntax) and
|
|
42
|
+
* cost nothing at runtime either way.
|
|
43
|
+
*/
|
|
44
|
+
// Ollama has no auth of its own — `pi-ai`'s auth resolution always calls
|
|
45
|
+
// `getClientApiKey()` before a dispatch, and that call throws "No API key
|
|
46
|
+
// for provider: ollama" if the resolved key is falsy (verified live: the
|
|
47
|
+
// upstream-documented `auth: { apiKey: {} }` recipe, with no `resolve()` or a
|
|
48
|
+
// resolver returning no key, throws exactly that at the FIRST dispatch, not
|
|
49
|
+
// at registration). There is no supported way to mark a provider as needing
|
|
50
|
+
// no key at all — the value below is a placeholder pi-ai never actually
|
|
51
|
+
// sends anywhere Ollama would look at it: Ollama's OpenAI-compatible server
|
|
52
|
+
// does not check the Authorization header's contents.
|
|
53
|
+
const DUMMY_API_KEY = "ollama-local-unused";
|
|
54
|
+
// Advisory only: pi-ai's `openai-completions` api reads this per REQUEST via
|
|
55
|
+
// its own `options.maxTokens`, not from `Model.maxTokens` directly — the
|
|
56
|
+
// field here only feeds Flue's compaction-reserve sizing (moot in practice
|
|
57
|
+
// since `contextWindow: 0` below disables threshold compaction, matching
|
|
58
|
+
// agent_flue.ts's own `context_window: 0`). Kept generous since Ollama
|
|
59
|
+
// enforces nothing against it.
|
|
60
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
61
|
+
/**
|
|
62
|
+
* Every `ollama/<id>` model id a `registerOllamaModel` call has ever
|
|
63
|
+
* SUCCEEDED in registering, in call order. `setProvider()` REPLACES the
|
|
64
|
+
* named provider's entire model list on every call — it is not additive —
|
|
65
|
+
* so this Set is what lets each call re-register the FULL union instead of
|
|
66
|
+
* just the newest id. Without it: register "a", then "b", and "a" becomes an
|
|
67
|
+
* unknown model id at its next dispatch (verified live: "Unknown model ID …
|
|
68
|
+
* for provider \"ollama\""), because the second `setProvider()` call
|
|
69
|
+
* replaced the first provider object — the one whose `models` list still
|
|
70
|
+
* had "a" — outright.
|
|
71
|
+
*
|
|
72
|
+
* `OLLAMA_BASE_URL` is read fresh (via `ollamaBaseUrl()`) at each
|
|
73
|
+
* registration call, and the whole union is re-registered at whatever URL
|
|
74
|
+
* is current AT THAT MOMENT — so a mid-process env change applies unevenly:
|
|
75
|
+
* ids already registered keep the base URL they were registered under until
|
|
76
|
+
* the NEXT new id triggers a fresh union re-registration, which then
|
|
77
|
+
* re-points every id at once. Deliberate: a single local server for the
|
|
78
|
+
* whole process is the supported case, and this asymmetry only bites a
|
|
79
|
+
* per-agent override, which isn't.
|
|
80
|
+
*/
|
|
81
|
+
const registeredIds = new Set();
|
|
82
|
+
// Registrations currently in flight, keyed by model id — lets a second
|
|
83
|
+
// caller for the SAME id that arrives before the first `await` resolves
|
|
84
|
+
// join that in-progress registration instead of returning immediately with
|
|
85
|
+
// nothing registered yet (which would let it dispatch before `setProvider()`
|
|
86
|
+
// has actually run).
|
|
87
|
+
const inflight = new Map();
|
|
88
|
+
// The most recently constructed provider object, kept only so tests can
|
|
89
|
+
// inspect its auth/model shape directly — this Flue version's public
|
|
90
|
+
// surface (`@flue/runtime/internal`) exports `setProvider`/`hasProvider`/
|
|
91
|
+
// `resolveModel` but no `getProvider`, so there is no other way to read a
|
|
92
|
+
// registered provider's `auth` back out of Flue's own registry.
|
|
93
|
+
let lastProvider;
|
|
94
|
+
/** 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. */
|
|
95
|
+
export function ollamaBaseUrl() {
|
|
96
|
+
const raw = (process.env.OLLAMA_BASE_URL ?? "").trim();
|
|
97
|
+
return raw || "http://localhost:11434/v1";
|
|
98
|
+
}
|
|
99
|
+
function modelFor(id, baseUrl) {
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
name: id,
|
|
103
|
+
api: "openai-completions",
|
|
104
|
+
provider: "ollama",
|
|
105
|
+
baseUrl,
|
|
106
|
+
reasoning: false,
|
|
107
|
+
input: ["text"],
|
|
108
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
109
|
+
// Disables Flue's threshold-based compaction outright — there's no
|
|
110
|
+
// reliable catalog of context windows for arbitrary local Ollama models,
|
|
111
|
+
// and agent_flue.ts already treats 0 as "unknown, don't compact" for its
|
|
112
|
+
// own reported `context_window`. Verified live to be a safe no-op, not
|
|
113
|
+
// silently truncating requests.
|
|
114
|
+
contextWindow: 0,
|
|
115
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
120
|
+
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
121
|
+
* id ever registered this process. Idempotent: a repeat of an already-seen
|
|
122
|
+
* id is a no-op — no re-registration, no re-import. A concurrent call for
|
|
123
|
+
* the SAME id joins the in-flight registration rather than returning early
|
|
124
|
+
* (see `inflight`'s doc); `registeredIds` itself is only ever updated AFTER
|
|
125
|
+
* `setProvider()` succeeds, so a failed attempt (a bad install, a bundler
|
|
126
|
+
* that can't resolve the deep `.lazy` subpath, a future validation error)
|
|
127
|
+
* leaves the id unregistered and eligible for a real retry — not
|
|
128
|
+
* permanently and misleadingly marked "done" while nothing is actually
|
|
129
|
+
* registered.
|
|
130
|
+
*
|
|
131
|
+
* Must complete before the FIRST Flue dispatch that names this model
|
|
132
|
+
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
133
|
+
* but is equally safe to call again later with a new id mid-process — that
|
|
134
|
+
* later call's union re-registration is exactly how a second model gets
|
|
135
|
+
* added without orphaning the first (see the `registeredIds` doc above).
|
|
136
|
+
*/
|
|
137
|
+
export async function registerOllamaModel(modelId) {
|
|
138
|
+
if (registeredIds.has(modelId))
|
|
139
|
+
return;
|
|
140
|
+
const existing = inflight.get(modelId);
|
|
141
|
+
if (existing)
|
|
142
|
+
return existing;
|
|
143
|
+
const promise = (async () => {
|
|
144
|
+
// Deliberately dynamic, not top-level, imports — see the module doc's
|
|
145
|
+
// "LAZY IMPORT" note. `@flue/runtime/internal` is cheap either way
|
|
146
|
+
// (it's already reachable from `@flue/runtime`/`@flue/runtime/node`,
|
|
147
|
+
// which agent_flue.ts imports unconditionally); pi-ai's own package is
|
|
148
|
+
// the one this module keeps off other modules' graphs.
|
|
149
|
+
const [{ createProvider }, { openAICompletionsApi }, { setProvider }] = await Promise.all([
|
|
150
|
+
import("@earendil-works/pi-ai"),
|
|
151
|
+
import("@earendil-works/pi-ai/api/openai-completions.lazy"),
|
|
152
|
+
import("@flue/runtime/internal"),
|
|
153
|
+
]);
|
|
154
|
+
// Built from a local candidate set, not `registeredIds` itself — the id
|
|
155
|
+
// being registered right now isn't committed to `registeredIds` until
|
|
156
|
+
// AFTER `setProvider()` below succeeds (see this function's doc).
|
|
157
|
+
const ids = new Set(registeredIds);
|
|
158
|
+
ids.add(modelId);
|
|
159
|
+
const baseUrl = ollamaBaseUrl();
|
|
160
|
+
const models = [...ids].map((id) => modelFor(id, baseUrl));
|
|
161
|
+
const options = {
|
|
162
|
+
id: "ollama",
|
|
163
|
+
name: "Ollama (local)",
|
|
164
|
+
baseUrl,
|
|
165
|
+
auth: {
|
|
166
|
+
apiKey: {
|
|
167
|
+
name: "Ollama (keyless)",
|
|
168
|
+
// See DUMMY_API_KEY above for why this can't just report "no key
|
|
169
|
+
// needed" — pi-ai's dispatch path requires a truthy resolved key.
|
|
170
|
+
resolve: async () => ({ auth: { apiKey: DUMMY_API_KEY } }),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
models,
|
|
174
|
+
api: openAICompletionsApi(),
|
|
175
|
+
};
|
|
176
|
+
// NOT `start({ providers: [...] })` — per @flue/runtime's own node/index
|
|
177
|
+
// typings, that option REPLACES the runtime's entire default provider
|
|
178
|
+
// set (every pi-ai built-in), which would silently drop
|
|
179
|
+
// anthropic/openai/etc. for every agent, not just ollama ones.
|
|
180
|
+
// `setProvider()` is the additive (per-id) primitive; it upserts this
|
|
181
|
+
// one id and leaves every other already-registered provider untouched.
|
|
182
|
+
const provider = createProvider(options);
|
|
183
|
+
setProvider(provider);
|
|
184
|
+
lastProvider = provider;
|
|
185
|
+
// Only commit to the Set once `setProvider()` has actually run —
|
|
186
|
+
// see this function's doc for why ordering this after, not before,
|
|
187
|
+
// matters.
|
|
188
|
+
for (const id of ids)
|
|
189
|
+
registeredIds.add(id);
|
|
190
|
+
})();
|
|
191
|
+
inflight.set(modelId, promise);
|
|
192
|
+
try {
|
|
193
|
+
await promise;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
inflight.delete(modelId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Test-only: the most recently constructed provider object (see `lastProvider`'s doc). */
|
|
200
|
+
export function providerForTest() {
|
|
201
|
+
return lastProvider;
|
|
202
|
+
}
|
|
203
|
+
/** 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`. */
|
|
204
|
+
export function resetOllamaRegistrationForTest() {
|
|
205
|
+
registeredIds.clear();
|
|
206
|
+
inflight.clear();
|
|
207
|
+
lastProvider = undefined;
|
|
208
|
+
}
|