fix: use native Pi identity and refresh provider catalogs

This commit is contained in:
amosblomqvist
2026-10-23 17:25:10 +03:00
parent 388addff3e
commit 6685b3d09a
15 changed files with 792 additions and 414 deletions
+9 -1
View File
@@ -11,6 +11,14 @@ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import { type ModelRole } from "../../config.ts";
import { DEFAULT_THINKING_ORDER } from "../core/settings.ts";
import packageMetadata from "../../package.json" with { type: "json" };
/** App identity is immutable and shared by Models and every native Agent.
* Agent's default Pi attribution otherwise overrides the Models identity. */
export const APP_IDENTITY = Object.freeze({
userAgent: Object.freeze({ custom: `learnharness/${packageMetadata.version}` }),
attribution: Object.freeze({ name: packageMetadata.name, url: packageMetadata.homepage }),
});
/**
* The one engine factory every agent's model comes from (the pi-ai swap,
@@ -47,7 +55,7 @@ let collection: ReturnType<typeof createModels> | undefined;
* keyPresent. */
export function piModels() {
if (!collection) {
collection = createModels();
collection = createModels({ identity: APP_IDENTITY });
for (const provider of builtinProviders()) collection.setProvider(provider);
}
return collection;
+5
View File
@@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
import type { AssistantMessage, Message, ToolResultMessage, Usage, Tool } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
import { Type } from "typebox";
@@ -183,6 +184,7 @@ export async function runPdfReaderAgent(
// BYOK (owner, 2026-09-22): the level rides the injected engine — no
// factory pin to consult (engine.level is the resolved/clamped value).
const reasoning = engine.level;
const sessionId = randomUUID();
const prompt = [
`PDF path: ${pdfPath}`,
`Question (what the teacher needs from this document): ${input.question || "(no question — give a structural overview)"}`,
@@ -207,6 +209,7 @@ export async function runPdfReaderAgent(
INSTRUCTIONS,
messages,
toolDeclarations,
sessionId,
reasoning,
opts.abortSignal,
opts.chatId ? providerEventObserver(opts.chatId, "job:pdfReader") : undefined,
@@ -395,6 +398,7 @@ async function streamOnce(
systemPrompt: string,
messages: Message[],
tools: Tool[],
sessionId: string,
reasoning: ThinkingLevel | undefined,
abortSignal?: AbortSignal,
onProviderStreamEvent?: ProviderEventObserver,
@@ -403,6 +407,7 @@ async function streamOnce(
// and tool declarations fold into a leading system message.
// normalizeContext is the library's only Context→TranscriptContext entry.
const stream = engine.streamFn(engine.model, normalizeContext({ systemPrompt, messages, tools }), {
sessionId,
reasoning: reasoning && reasoning !== "off" ? reasoning : undefined,
signal: abortSignal,
onProviderStreamEvent: onProviderStreamEvent as never,
+5 -1
View File
@@ -1,6 +1,7 @@
import { Agent, type AgentEvent, type AgentMessage, type AgentTool } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, ToolResultMessage, Usage } from "@earendil-works/pi-ai";
import { piEngineFor, type PiEngine } from "../models.ts";
import { randomUUID } from "node:crypto";
import { APP_IDENTITY, piEngineFor, type PiEngine } from "../models.ts";
import { providerEventObserver } from "../provider-events.ts";
import type { ModelRole } from "../../../config.ts";
@@ -64,6 +65,9 @@ export async function runSubagent(tools: AgentTool<any, any>[], opts: SubagentRu
// Debug recorder (owner, 2026-10-04): raw provider events per job run.
const providerEvents = opts.chatId ? providerEventObserver(opts.chatId, `job:${opts.role}`) : undefined;
const agent = new Agent({
identity: APP_IDENTITY,
// One unsaved conversation per invocation, stable through every turn.
sessionId: randomUUID(),
initialState: {
systemPrompt: opts.systemPrompt,
model: engine.model,
+5
View File
@@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
import type { AssistantMessage, Message, Tool, ToolResultMessage, Usage } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
import { Type } from "typebox";
@@ -142,6 +143,7 @@ export async function runVisualMaker(input: VisualMakerInput, opts: VisualMakerO
// BYOK (owner, 2026-09-22): the level rides the injected engine — no
// factory pin to consult (engine.level is the resolved/clamped value).
const reasoning = engine.level;
const sessionId = randomUUID();
const prompt = [
"Brief for ONE precise figure (from the teacher, verbatim — it is everything you know):",
brief,
@@ -164,6 +166,7 @@ export async function runVisualMaker(input: VisualMakerInput, opts: VisualMakerO
INSTRUCTIONS,
messages,
toolDeclarations,
sessionId,
reasoning,
opts.abortSignal,
opts.chatId ? providerEventObserver(opts.chatId, "job:visualMaker") : undefined,
@@ -317,6 +320,7 @@ async function streamOnce(
systemPrompt: string,
messages: Message[],
tools: Tool[],
sessionId: string,
reasoning: ThinkingLevel | undefined,
abortSignal?: AbortSignal,
onProviderStreamEvent?: ProviderEventObserver,
@@ -325,6 +329,7 @@ async function streamOnce(
// and tool declarations fold into a leading system message.
// normalizeContext is the library's only Context→TranscriptContext entry.
const stream = engine.streamFn(engine.model, normalizeContext({ systemPrompt, messages, tools }), {
sessionId,
reasoning: reasoning && reasoning !== "off" ? reasoning : undefined,
signal: abortSignal,
onProviderStreamEvent: onProviderStreamEvent as never,
+6 -2
View File
@@ -4,7 +4,7 @@ import { invalidInputResult, questionResult } from "./tool-execute.ts";
import { providerEventObserver } from "./provider-events.ts";
import type { Usage } from "@earendil-works/pi-ai";
import { PATHS } from "../../config.ts";
import { piEngineFor, type PiEngine } from "./models.ts";
import { APP_IDENTITY, piEngineFor, type PiEngine } from "./models.ts";
import { effectivePromptSegments } from "./prompt-segments.ts";
import {
askUserQuestionTool,
@@ -116,7 +116,9 @@ export interface AgentBuildOptions {
* fresh per turn, honoring enabled overrides (switch on + file
* exists); absent → the cached pure-source composition. */
promptSwitches?: Record<string, boolean>;
/** Debug: when set AND LH_PROVIDER_EVENTS=1, raw provider stream events
/** Native conversation identity: the existing persisted chat id, reused
* across reconstructed prober, teacher, and practice Agents.
* Debug: when set AND LH_PROVIDER_EVENTS=1, raw provider stream events
* (pre-normalization) append to data/debug/provider-events/
* <chatId>.jsonl via providerEventObserver — the GLM-truncation /
* DeepSeek-id incident layer. Absent or env-off → no observer. */
@@ -177,6 +179,8 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
// the normal no-observer path (tests, env-off).
const providerEvents = opts.chatId ? providerEventObserver(opts.chatId, kind) : undefined;
const agent = new Agent({
identity: APP_IDENTITY,
sessionId: opts.chatId,
initialState: {
systemPrompt,
model: engine.model,
+1 -1
View File
@@ -77,7 +77,7 @@ export interface AuthProviderInfo {
*
* EXCLUDED by owner spec — providers whose auth is not a plain key this
* wave: github-copilot (OAuth), amazon-bedrock (AWS creds), google-vertex
* (ADC), azure-openai-responses (needs a base URL), the cloudflare
* (ADC), azure (needs a base URL), the cloudflare
* account-scoped pair. Anthropic keeps ANTHROPIC_API_KEY only — its
* AUTH_TOKEN/OAUTH_TOKEN alternates stay env-only. openai-codex exposes
* no env-key (naturally absent). Providers sharing one env var (moonshotai
+318 -256
View File
File diff suppressed because it is too large Load Diff