refactor: extract reusable AI runtime package (#99059)

* refactor: extract reusable AI runtime package

* refactor: complete AI provider relocation

* refactor: keep llm core internal

* refactor(ai): make @openclaw/ai self-contained with host policy ports

Move pure transport helpers (tool projections, strict-schema normalization,
prompt-cache boundary, stream guards, anthropic/openai compat, request
activity) from src into packages/ai; move utf16-slice into
normalization-core. Inject host policy (guarded fetch, redaction,
strict-tool defaults, diagnostics logging) through AiTransportHost with
inert library defaults installed by src/llm/stream.ts. Narrow the public
barrel to instance-scoped createApiRegistry/createLlmRuntime; the
process-default runtime moves behind internal/ and
registerBuiltInApiProviders takes an explicit registry. Delete the
src/llm/api-registry re-export facade.

* fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths

The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver,
sdk-alias, the shared vitest config, and the Control UI vite config only
knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai
through the pnpm symlink to the unbuilt dist (checks-node-compact CI
failures), and the Control UI build broke on the new
normalization-core/utf16-slice subpath.

* chore(ui): drop leftover service-worker debug logging

* build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set

packages/ai declares only its six real runtime deps (kysely, chalk, json5,
tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps
removed. generate-npm-shrinkwrap now treats publishable packages/* like
publishable plugins so the AI tarball pins its transitive tree even though
workspace deps are omitted from the root shrinkwrap. knip learns the
package entry points; the tsdown dts neverBundle option moves to its
documented deps.dts home; the README documents the no-semver internal/*
contract and host ports.

* docs(ai): add minimal external-consumer example app

examples/ai-chat consumes only the public @openclaw/ai surface (built dist
via the workspace link): isolated runtime, built-in provider registration,
one streamed completion. Supports Anthropic/OpenAI via env keys and a
keyless local Ollama target; live-verified against Ollama.

* docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary

* chore(check): include examples/ in duplicate-scan targets

* fix: emit normalization package subpaths

* fix: complete AI package boundary artifacts

* fix: align AI package boundary contracts

* fix(ci): stabilize package release contracts

* test: align documentation contract checks

* test: keep cron docs guard aligned

* test: align restored docs contract guards

* test: follow upstream docs contracts

* docs: drop superseded talk wording
This commit is contained in:
Peter Steinberger
2026-07-05 01:56:40 -04:00
committed by GitHub
parent 913311845e
commit 062f88e3e3
230 changed files with 3122 additions and 1248 deletions

View File

@@ -170,6 +170,23 @@ jobs:
- name: Build
run: pnpm build
- name: Pack AI runtime package
id: ai_runtime_tarballs
run: |
set -euo pipefail
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
package_version="$(node -p "require('./packages/ai/package.json').version")"
if [[ "$package_version" != "$PACKAGE_VERSION" ]]; then
echo "packages/ai version ${package_version} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-ai-runtime-packages"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
pnpm --dir packages/ai pack --pack-destination "$ARTIFACT_DIR"
(cd "$ARTIFACT_DIR" && sha256sum ./*.tgz > SHA256SUMS)
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
- name: Build Control UI
run: pnpm ui:build
@@ -233,10 +250,11 @@ jobs:
RELEASE_REF: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
AI_RUNTIME_TARBALL_DIR: ${{ steps.ai_runtime_tarballs.outputs.dir }}
run: |
set -euo pipefail
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
npm pack --json 2>&1 | tee "$PACK_OUTPUT"
pnpm pack --json 2>&1 | tee "$PACK_OUTPUT"
PACK_NAME="$(node - "$PACK_OUTPUT" <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
@@ -244,19 +262,23 @@ jobs:
function resolveTarballFileName(value) {
const fileName = typeof value === "string" ? value.trim() : "";
const resolved = path.resolve(fileName);
if (
!fileName.endsWith(".tgz") ||
fileName.includes("\0") ||
fileName !== path.basename(fileName) ||
fileName !== path.win32.basename(fileName)
fileName !== path.win32.basename(fileName) ||
path.dirname(resolved) !== process.cwd()
) {
console.error(`npm pack reported unsafe tarball filename ${JSON.stringify(fileName)}.`);
process.exit(1);
}
return fileName;
return path.basename(resolved);
}
function arrayEndFrom(start) {
function jsonEndFrom(start) {
const opening = input[start];
const closing = opening === "[" ? "]" : "}";
let depth = 0;
let inString = false;
let escape = false;
@@ -274,9 +296,9 @@ jobs:
}
if (char === "\"") {
inString = true;
} else if (char === "[") {
} else if (char === opening) {
depth += 1;
} else if (char === "]") {
} else if (char === closing) {
depth -= 1;
if (depth === 0) {
return i + 1;
@@ -286,15 +308,15 @@ jobs:
return -1;
}
for (const match of input.matchAll(/\[/g)) {
for (const match of input.matchAll(/[\[{]/g)) {
const start = match.index;
const end = arrayEndFrom(start);
const end = jsonEndFrom(start);
if (end === -1) {
continue;
}
try {
const parsed = JSON.parse(input.slice(start, end));
const first = Array.isArray(parsed) ? parsed[0] : null;
const first = Array.isArray(parsed) ? parsed[0] : parsed;
if (first && Object.prototype.hasOwnProperty.call(first, "filename")) {
process.stdout.write(resolveTarballFileName(first.filename));
process.exit(0);
@@ -326,6 +348,8 @@ jobs:
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
cp "$PACK_PATH" "$ARTIFACT_DIR/"
cp "$AI_RUNTIME_TARBALL_DIR"/*.tgz "$ARTIFACT_DIR/"
cp "$AI_RUNTIME_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/ai-runtime-SHA256SUMS"
cp -R "$DEPENDENCY_EVIDENCE_DIR" "$ARTIFACT_DIR/dependency-evidence"
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
@@ -358,14 +382,16 @@ jobs:
PREFLIGHT_ARTIFACT_DIR: ${{ steps.packed_tarball.outputs.dir }}
run: |
set -euo pipefail
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name '*.tgz' -print | sort | tail -n 1)"
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name 'openclaw-[0-9]*.tgz' -print | sort | tail -n 1)"
if [[ -z "$TARBALL_PATH" ]]; then
echo "Prepared preflight tarball not found." >&2
ls -la "$PREFLIGHT_ARTIFACT_DIR" >&2 || true
exit 1
fi
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION"
AI_TARBALL="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | head -n 1)"
node --import tsx scripts/openclaw-npm-prepublish-verify.ts \
"$TARBALL_PATH" "$PACKAGE_VERSION" "$AI_TARBALL"
- name: Upload dependency release evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@@ -716,6 +742,11 @@ jobs:
echo "Prepared preflight tarball digest mismatch." >&2
exit 1
fi
if [[ ! -f preflight-tarball/ai-runtime-SHA256SUMS ]]; then
echo "Prepared AI runtime dependency checksums are missing." >&2
exit 1
fi
(cd preflight-tarball && sha256sum -c ai-runtime-SHA256SUMS)
echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
@@ -779,6 +810,22 @@ jobs:
if [[ -n "${publish_target}" ]]; then
publish_target="./${publish_target}"
fi
publish_if_missing() {
local package_name="$1"
local tarball_path="$2"
local package_version
if [[ -z "$tarball_path" || ! -f "$tarball_path" ]]; then
echo "Prepared tarball for ${package_name} was not found." >&2
exit 1
fi
package_version="$(node -p "require('./package.json').version")"
if npm view "${package_name}@${package_version}" version >/dev/null 2>&1; then
echo "${package_name}@${package_version} is already published; reusing it."
return 0
fi
bash scripts/openclaw-npm-publish.sh --publish "./${tarball_path}"
}
publish_if_missing "@openclaw/ai" "$(find preflight-tarball -type f -name 'openclaw-ai-*.tgz' -print | head -n 1)"
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
- name: Verify extended-stable registry readback