mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai,durable): add lightweight pi-ai/models entry and avoid TypeBox in durable
Durable generation imports retry helpers from pi-ai/utils/retry; examples use pi-ai/models and pi-ai/providers/faux. Entry graph budgets and an import footprint test guard the models entry.
This commit is contained in:
parent
0a7a4eca68
commit
ba7d5fbedb
17 changed files with 107 additions and 31 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added the lightweight `@earendil-works/pi-ai/models` entry point for model collections and provider construction without loading TypeBox, built-in catalogs, or provider SDKs.
|
||||
|
||||
## [0.99.1] - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1653,10 +1653,10 @@ Browser compatibility notes:
|
|||
|
||||
## Bundling and Tree Shaking
|
||||
|
||||
For small bundles, import only the providers you need:
|
||||
For small bundles and low-overhead unbundled scripts, import the model runtime and only the providers you need:
|
||||
|
||||
```typescript
|
||||
import { createModels } from '@earendil-works/pi-ai';
|
||||
import { createModels } from '@earendil-works/pi-ai/models';
|
||||
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
|
||||
|
||||
const models = createModels();
|
||||
|
|
@ -1665,7 +1665,8 @@ models.setProvider(openaiProvider());
|
|||
|
||||
Rules:
|
||||
|
||||
- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, provider factories, or SDK implementations.
|
||||
- `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without TypeBox, built-in catalogs, or SDK implementations. Other types can still use `import type` from the root.
|
||||
- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports TypeBox and schema validation. Unbundled Node scripts do not tree-shake its unused exports; prefer `./models`, `./providers/faux`, and specific `./utils/*` subpaths when those are all you need.
|
||||
- `@earendil-works/pi-ai/providers/<provider>` imports that provider's catalog and lazy API wrapper only.
|
||||
- `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set.
|
||||
- With code splitting, provider SDKs stay in lazy chunks and load on first request.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./models": {
|
||||
"types": "./dist/models.d.ts",
|
||||
"import": "./dist/models.js"
|
||||
},
|
||||
"./compat": {
|
||||
"types": "./dist/compat.d.ts",
|
||||
"import": "./dist/compat.js"
|
||||
|
|
|
|||
58
packages/ai/test/models-entry.test.ts
Normal file
58
packages/ai/test/models-entry.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
const sourceEntry = new URL("../src/models.ts", import.meta.url).href;
|
||||
|
||||
// Install before linking the static imports, so unused barrel re-exports are checked too.
|
||||
const preload = `data:text/javascript,${encodeURIComponent(`
|
||||
import { registerHooks } from "node:module";
|
||||
const forbidden = [
|
||||
"/node_modules/typebox/",
|
||||
"/node_modules/@anthropic-ai/sdk/",
|
||||
"/node_modules/openai/",
|
||||
"/node_modules/@google/genai/",
|
||||
"/node_modules/@aws-sdk/",
|
||||
"/providers/data/",
|
||||
"/models.generated.",
|
||||
];
|
||||
registerHooks({
|
||||
load(url, context, nextLoad) {
|
||||
if (forbidden.some((part) => url.includes(part))) {
|
||||
throw new Error("Heavy dependency loaded by models entry: " + url);
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
},
|
||||
});
|
||||
`)}`;
|
||||
|
||||
describe("lightweight models entry", () => {
|
||||
it.each([sourceEntry, "@earendil-works/pi-ai/models"])(
|
||||
"runs a faux completion without TypeBox, catalogs, or SDKs through %s",
|
||||
(entry) => {
|
||||
const script = `
|
||||
import assert from "node:assert/strict";
|
||||
import { createModels, createProvider } from ${JSON.stringify(entry)};
|
||||
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
||||
assert.equal(typeof createProvider, "function");
|
||||
const models = createModels();
|
||||
assert.deepEqual(models.getModels(), []);
|
||||
assert.deepEqual(models.getProviders(), []);
|
||||
const faux = fauxProvider();
|
||||
models.setProvider(faux.provider);
|
||||
faux.setResponses([fauxAssistantMessage("OK")]);
|
||||
const response = await models.completeSimple(faux.getModel(), { messages: [] });
|
||||
assert.equal(response.stopReason, "stop");
|
||||
assert.deepEqual(response.content, [{ type: "text", text: "OK" }]);
|
||||
`;
|
||||
const result = spawnSync(process.execPath, ["--import", preload, "--input-type=module", "--eval", script], {
|
||||
cwd: packageRoot,
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
### Fixed
|
||||
|
||||
- Fixed cached documents skipping migration when accessed with a newer definition version, and older definitions reading values migrated only in memory. Document states and watches hydrated under another definition version receive the new value as a root replacement.
|
||||
- Avoided loading TypeBox through the package root's generation retry helpers and switched examples to narrow pi-ai model and faux-provider imports.
|
||||
|
||||
## [0.87.1] - 2026-09-22
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { type Context, copyJson, type Draft, type JsonValue } from "@earendil-works/chord";
|
||||
import {
|
||||
type Api,
|
||||
type AssistantMessage,
|
||||
type DeferredHandle,
|
||||
isRetryableAssistantError,
|
||||
type Message,
|
||||
type Model,
|
||||
type ModelThinkingLevel,
|
||||
retryDelayMs,
|
||||
type SimpleStreamOptions,
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
DeferredHandle,
|
||||
Message,
|
||||
Model,
|
||||
ModelThinkingLevel,
|
||||
SimpleStreamOptions,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { isRetryableAssistantError, retryDelayMs } from "@earendil-works/pi-ai/utils/retry";
|
||||
import { AssistantEntry, SystemEntry } from "../entries.ts";
|
||||
import { defineTask } from "../tasks.ts";
|
||||
import type { ConversationId, EntryId, NextTaskState, TaskRuntime, Tx } from "../types.ts";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/06-harness.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels, Type } from "@earendil-works/pi-ai";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import {
|
||||
ConversationConfig,
|
||||
createRegistry,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/07-configuration.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels, Type } from "@earendil-works/pi-ai";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, Harness, MemoryStorage, type ToolRegistration } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/08-harness-conversations.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { ConversationConfig, createRegistry, defineEntry, Harness, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,8 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/09-context.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import {
|
||||
type AssistantMessage,
|
||||
createModels,
|
||||
type Message,
|
||||
type StopReason,
|
||||
type ToolResultMessage,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import type { AssistantMessage, Message, StopReason, ToolResultMessage } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, Harness, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/11-conversation-setup.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, defineDoc, Harness, LiveDoc, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/12-tasks.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, defineTask, Harness, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, defineTask, Harness } from "../../src/index.ts";
|
||||
import { openNodeSqliteStorage } from "../../src/storage/sqlite/node.ts";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/14-chat.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels, fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { AssistantEntry, ConversationConfig, createRegistry, Harness, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/15-system-prompt.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels, fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
||||
import {
|
||||
type Conversation,
|
||||
ConversationConfig,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Run from packages/durable (needs OPENAI_API_KEY):
|
||||
// node --conditions=source --experimental-strip-types test/examples/16-real-model.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
|
||||
import { AssistantEntry, createRegistry, Harness, LiveDoc, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,23 @@ const WORKSPACE = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Budgets are deliberate. `.` and `./node` are batteries-included entries and stay unbounded; every
|
||||
* narrow entry states the graph it is allowed to reach.
|
||||
* Budgets are deliberate. Entries with no budget remain unbounded; each listed entry states the
|
||||
* graph it is allowed to reach.
|
||||
*/
|
||||
const BUDGETS = {
|
||||
"packages/ai": {
|
||||
"./models": {
|
||||
maxFiles: 15,
|
||||
forbid: ["providers/", "models.generated.ts", "index.ts", "utils/validation.ts", "utils/typebox-helpers.ts"],
|
||||
},
|
||||
"./utils/*": { maxFiles: 3, forbid: ["providers/", "api/", "index.ts"] },
|
||||
},
|
||||
"packages/durable": {
|
||||
".": {
|
||||
maxFiles: 50,
|
||||
forbid: ["packages/ai/src/index.ts", "packages/ai/src/utils/validation.ts", "packages/ai/src/utils/typebox-helpers.ts"],
|
||||
},
|
||||
},
|
||||
"packages/agent": {
|
||||
"./harness/runtime/reducer": { maxFiles: 1 },
|
||||
"./harness/context": { maxFiles: 6, forbid: ["harness/runtime/", "harness/execution/", "packages/ai/"] },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue