Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ ui-dist
.codanna/*
.fastembed_cache
.memory
.env
.env
examples/local
examples/*.zip
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ workflow — research, code generation, document building, scheduled automation,
and tool calls.
- **Agent Bundles** — Portable `.zip` packages that preconfigure a session with a system
prompt, tool allowlist, skills, workflows, sub-agent templates, seed memory, triggers, and
optional custom UI. A handful of ready-to-use bundles ship in [`examples/configs`](examples/configs).
optional custom UI. A handful of ready-to-use bundles ship in [`examples`](examples).
- **Sub-agent orchestration** — Prime delegates work to specialist sub-agents (researcher,
builder, reviewer, …) that run in parallel in a shared workspace and report back.
- **Memory** — Per-session memory plus a global, cross-session store the agent reads and
Expand Down Expand Up @@ -107,7 +107,7 @@ pnpm dev # runs server + web via Turbo
To load the example agent bundles into your local marketplace:

```bash
pnpm seed # installs bundles from examples/configs
pnpm seed # installs bundles from examples
```

### Configuration
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/pi/config/bundleLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import { installBundle, loadInstalledConfig } from "./bundleLoader.ts";
const here = path.dirname(fileURLToPath(import.meta.url));
// apps/server/src/pi/config -> repo root is five levels up.
const repoRoot = path.resolve(here, "../../../../..");
const EXAMPLE_BUNDLE = path.join(
repoRoot,
"examples/configs/research-assistant",
);
const EXAMPLE_BUNDLE = path.join(repoRoot, "examples/research-assistant");

/** Collects a bundle source dir into the POSIX-keyed entries fflate zips. */
function collectFiles(
Expand Down
59 changes: 55 additions & 4 deletions apps/server/src/store/seedBundles.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import {
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";

import { zipSync } from "fflate";

import { FileAgentBundleStore } from "./fileAgentBundleStore.ts";
import { seedExampleBundles } from "./seedBundles.ts";

const here = path.dirname(fileURLToPath(import.meta.url));
// apps/server/src/store -> repo root is four levels up.
const repoRoot = path.resolve(here, "../../../..");
const EXAMPLES_DIR = path.join(repoRoot, "examples/configs");
const EXAMPLES_SRC = path.join(repoRoot, "examples");

/** Collects a bundle source dir into the POSIX-keyed entries fflate zips. */
function collectFiles(
root: string,
dir: string,
files: Record<string, Uint8Array>,
): Record<string, Uint8Array> {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
collectFiles(root, abs, files);
continue;
}
const rel = path.relative(root, abs).split(path.sep).join("/");
files[rel] = new Uint8Array(readFileSync(abs));
}
return files;
}

/** Packs every `examples/<name>/` source folder into `<dir>/<name>.zip`. */
function packExamplesInto(dir: string): void {
for (const entry of readdirSync(EXAMPLES_SRC, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const src = path.join(EXAMPLES_SRC, entry.name);
const files = collectFiles(src, src, {});
if (!("tangent.yaml" in files)) continue;
writeFileSync(
path.join(dir, `${entry.name}.zip`),
Buffer.from(zipSync(files)),
);
}
}

/** Runs `body` with `SEED_BUNDLES_DIR` set, restoring it afterwards. */
async function withSeedDir(dir: string, body: () => Promise<void>) {
Expand All @@ -25,6 +65,17 @@ async function withSeedDir(dir: string, body: () => Promise<void>) {
}
}

/** Packs the real example sources into a throwaway zip dir for `body`. */
async function withPackedExamples(body: (dir: string) => Promise<void>) {
const dir = mkdtempSync(path.join(tmpdir(), "seed-src-"));
packExamplesInto(dir);
try {
await withSeedDir(dir, () => body(dir));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

/** A FileAgentBundleStore backed by a throwaway temp directory. */
function tempStore(): { store: FileAgentBundleStore; cleanup: () => void } {
const root = mkdtempSync(path.join(tmpdir(), "seed-bundles-"));
Expand All @@ -37,7 +88,7 @@ function tempStore(): { store: FileAgentBundleStore; cleanup: () => void } {
test("seeds the example bundles into an empty marketplace", async () => {
const { store, cleanup } = tempStore();
try {
await withSeedDir(EXAMPLES_DIR, async () => {
await withPackedExamples(async () => {
const installed = await seedExampleBundles(store);
const bundles = await store.list();

Expand All @@ -56,7 +107,7 @@ test("seeds the example bundles into an empty marketplace", async () => {
test("is a no-op against an already-populated marketplace", async () => {
const { store, cleanup } = tempStore();
try {
await withSeedDir(EXAMPLES_DIR, async () => {
await withPackedExamples(async () => {
const first = await seedExampleBundles(store);
assert.ok(first >= 1);

Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/store/seedBundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from "node:path";

import type { AgentBundleStore } from "./agentBundleStore.ts";

// Set by the `pnpm seed` script (to "$PWD/examples/configs").
// Set by the `pnpm seed` script (to "$PWD/examples").
function exampleBundlesDir(): string {
const dir = process.env.SEED_BUNDLES_DIR;
if (!dir) throw new Error("SEED_BUNDLES_DIR is not set; run via `pnpm seed`");
Expand Down
2 changes: 1 addition & 1 deletion docs/overview/03-agent-bundles.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ everything else is optional and auto-discovered when present.

## A real example: the Tangle ML Pipeline Optimizer

The example bundle in `examples/configs/tangle-ml-pipeline-optimizer` is a
The example bundle in `examples/tangle-ml-pipeline-optimizer` is a
working expert. Its manifest declares who it is, the exact tools Prime may use
(including custom `tangle_*` tools that call a real ML platform), a set of
specialist sub-agents, and three custom UI components:
Expand Down
Binary file removed examples/configs/presentation-deck-agent.zip
Binary file not shown.
Binary file removed examples/configs/research-assistant.zip
Binary file not shown.
Binary file removed examples/configs/simple-html-page-agent.zip
Binary file not shown.
Binary file removed examples/configs/skill-builder.zip
Binary file not shown.
Binary file removed examples/configs/tangle-ml-pipeline-optimizer.zip
Binary file not shown.
Binary file removed examples/configs/tangle.zip
Binary file not shown.
File renamed without changes
File renamed without changes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"dev": "SESSIONS_ROOT=\"$PWD/.sessions\" SESSIONS_DB=\"$PWD/.sessions/tangent.db\" AGENT_BUNDLES_ROOT=\"$PWD/.agent-bundles\" GLOBAL_MEMORY_DIR=\"$PWD/.memory\" turbo run dev",
"oasis:token": "tsx scripts/print-oasis-token.ts",
"seed": "AGENT_BUNDLES_ROOT=\"$PWD/.agent-bundles\" SEED_BUNDLES_DIR=\"$PWD/examples/configs\" pnpm --filter @tangent/server seed",
"seed": "AGENT_BUNDLES_ROOT=\"$PWD/.agent-bundles\" SEED_BUNDLES_DIR=\"$PWD/examples\" pnpm --filter @tangent/server seed",
"build": "turbo run build",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
Expand Down
10 changes: 5 additions & 5 deletions scripts/deploy-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#
# Arguments:
# SOURCE_DIR Bundle source folder (default:
# examples/configs/tangle-ml-pipeline-optimizer).
# examples/tangle-ml-pipeline-optimizer).
# May also be passed via -s/--source.
#
# Options:
Expand All @@ -25,14 +25,14 @@
# -h, --help Show this help.
#
# The bundle id and version are read from <SOURCE_DIR>/tangent.yaml. The packed
# archive lands at examples/configs/<id>.zip (see scripts/pack-bundle.mjs).
# archive lands at examples/<id>.zip (see scripts/pack-bundle.mjs).

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

SOURCE_DIR="examples/configs/tangle-ml-pipeline-optimizer"
SOURCE_DIR="examples/tangle-ml-pipeline-optimizer"
BUMP="minor"
SERVER="http://localhost:8787"
DO_INSTALL=1
Expand Down Expand Up @@ -99,9 +99,9 @@ deploy_one() {
echo "Bundle: $BUNDLE_ID@$BUNDLE_VERSION"
echo "Source: $SOURCE_DIR"

# Pack into examples/configs/<id>.zip.
# Pack into examples/<id>.zip.
pnpm pack:bundle "$SOURCE_DIR"
ZIP_PATH="examples/configs/$BUNDLE_ID.zip"
ZIP_PATH="examples/$BUNDLE_ID.zip"

if [[ "$DO_INSTALL" -eq 0 ]]; then
echo "Packed (install skipped): $ZIP_PATH"
Expand Down
8 changes: 4 additions & 4 deletions scripts/pack-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* Usage:
* node scripts/pack-bundle.mjs [sourceDir]
*
* `sourceDir` defaults to `examples/configs/research-assistant`. The folder's
* `sourceDir` defaults to `examples/tangle-oss`. The folder's
* `tangent.yaml` is read to derive the bundle `id`, and the archive is written
* to `examples/configs/<id>.zip`.
* to `examples/<id>.zip`.
*
* The walk skips dotfiles and OS junk, uses forward-slash relative paths, and
* rejects any entry that would escape the source root. A fixed mtime is used so
Expand All @@ -22,8 +22,8 @@ import { parse as parseYaml } from "yaml";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");

const DEFAULT_SOURCE = "examples/configs/research-assistant";
const OUTPUT_DIR = "examples/configs";
const DEFAULT_SOURCE = "examples/tangle-oss";
const OUTPUT_DIR = "examples";

/** Skip OS junk and any dotfile/dotdir. */
function shouldSkip(name) {
Expand Down
Loading