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
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ jobs:
echo "artifact_url=$artifact_url"
} >> "$GITHUB_OUTPUT"

# Mirror block/sessh: mint a scoped token for the OSS Homebrew tap and
# trigger its bump-formula workflow so `brew install block/tap/ghost`
# tracks releases automatically. block -> block, no Square refs. Gated on
# Mint a scoped token for the OSS Homebrew tap and trigger its
# bump-formula workflow so `brew install block/tap/ghost` tracks releases
# automatically. Gated on
# an actual publish and on the BLOCK_HOMEBREW_TAP_* secrets (GitHub App
# installed on this repo), so forks and unconfigured environments skip it
# cleanly instead of failing the release.
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ docs/superpowers/plans/
docs/superpowers/specs/
notes/
/demo/
# Local Ghost selection event tapes
**/.ghost/.events
**/fingerprint/.events
26 changes: 2 additions & 24 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,24 +1,2 @@
# This CODEOWNERS file denotes the project leads
# and encodes their responsibilities for code review.

# Instructions: At a minimum, replace the '@GITHUB_USER_NAME_GOES_HERE'
# here with at least one project lead.

# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# The format is described: https://github.blog/2017-07-06-introducing-code-owners/

# These owners will be the default owners for everything in the repo.
* @nahiyankhan


# -----------------------------------------------
# BELOW THIS LINE ARE TEMPLATES, UNUSED
# -----------------------------------------------
# Order is important. The last matching pattern has the most precedence.
# So if a pull request only touches javascript files, only these owners
# will be requested to review.
# *.js @octocat @github/js

# You can also use email addresses if you prefer.
# docs/* docs@example.com
# Default owner for all files.
* @nahiyankhan
25 changes: 0 additions & 25 deletions apps/docs/src/components/docs/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,6 @@ export const Icons = {
/>
</svg>
),
cashapp: (props: IconProps) => (
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
{...props}
viewBox="0 0 210 210"
fill="none"
>
<title>Cash App Logo</title>
<rect
width="150"
height="150"
x="25"
y="25"
rx="7.81424"
fill="#fff"
></rect>
<path
fill="#00e013"
fillRule="evenodd"
clipRule="evenodd"
d="M206.398,30.286c-4.502-12.394-14.307-22.189-26.713-26.687C168.48,0,158.075,0,136.865,0h-63.63 c-21.11,0-31.615,0-42.92,3.498C17.909,7.996,8.104,17.792,3.602,30.186C0,41.58,0,52.075,0,73.165v63.57 c0,21.19,0,31.585,3.502,42.98c4.502,12.394,14.307,22.189,26.713,26.687C41.62,210,52.125,210,73.135,210h63.63 c21.11,0,31.615,0,42.92-3.598c12.406-4.498,22.211-14.293,26.713-26.687C210,168.32,210,157.825,210,136.735v-63.47 C210,52.175,210,41.68,206.398,30.286z M151.472,71.866l-12.106,9.895c-1.101,0.9-2.601,0.7-3.402-0.4 c-6.203-7.596-15.807-11.894-26.312-11.894c-11.706,0-19.009,5.098-19.009,12.294c-0.2,6.097,5.503,9.196,23.011,12.994 c22.111,4.698,32.215,13.893,32.215,29.386c0,19.391-15.808,33.684-40.519,35.283l-2.401,11.495 c-0.2,1.099-1.201,1.899-2.401,1.899H81.539c-1.601,0-2.701-1.499-2.401-2.999l3.001-12.794 c-12.206-3.498-22.111-10.295-27.813-18.591c-0.7-1.099-0.5-2.499,0.5-3.298l13.206-10.395c1.101-0.9,2.701-0.6,3.502,0.5 c7.003,9.795,17.808,15.593,30.815,15.593c11.706,0,20.51-5.697,20.51-13.893c0-6.297-4.402-9.196-19.309-12.294 c-25.412-5.497-35.517-14.793-35.517-30.286c0-17.991,15.107-31.485,37.918-33.284l2.501-11.894c0.2-1.099,1.201-1.899,2.401-1.899 h18.709c1.501,0,2.701,1.399,2.401,2.899l-2.901,13.294c9.805,2.999,17.808,8.396,22.811,15.093 C152.673,69.567,152.473,71.066,151.472,71.866z"
></path>
</svg>
),
paypal: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
Expand Down
16 changes: 7 additions & 9 deletions packages/context-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,13 @@ adapters ship, selectable in the bench UI:
- `fake-lexical` — deterministic lexical-overlap stub with per-trial
jitter near the decision boundary. No network, instant. Use it to
exercise the UI loop and as a dumb-retrieval baseline.
- `databricks` — a real LLM behind a Databricks serving endpoint with an
OpenAI-compatible chat API. This is the default whenever
`DATABRICKS_HOST` is set; put it in an untracked `.env` or `.env.local`
at the working directory and the CLI loads it on startup. Auth comes
from the `databricks` CLI's cached OAuth token; the endpoint from
`CONTEXT_CONTROL_ENDPOINT` (default `goose`), or name it inline as
`databricks:<endpoint>`. Trials run concurrently and sample at the
endpoint's default temperature — trial-to-trial variance is the signal
being measured, so it is not pinned to 0.
- `openai-compatible` — a real LLM behind any OpenAI-compatible chat API.
Configure `CONTEXT_CONTROL_BASE_URL`, `CONTEXT_CONTROL_API_KEY`, and
`CONTEXT_CONTROL_MODEL` in an untracked `.env` or `.env.local` at the
working directory. The CLI loads it on startup and makes this adapter the
default when all three values are present. Trials run concurrently and
sample at the endpoint's default temperature because trial-to-trial variance
is the signal being measured.

Add providers to `MODEL_ADAPTERS` in `lib/model.mjs`.

Expand Down
4 changes: 2 additions & 2 deletions packages/context-control/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { resolve } from "node:path";
import { defaultGhostBin } from "./lib/ghost.mjs";
import { startServer } from "./lib/server.mjs";

// Load .env from the working directory if present (DATABRICKS_HOST lives
// in an untracked .env / .env.local, same convention as the ghost CLI).
// Load optional model-adapter configuration from untracked env files, using
// the same convention as the ghost CLI.
for (const envFile of [".env", ".env.local"]) {
const envPath = resolve(process.cwd(), envFile);
if (existsSync(envPath)) {
Expand Down
77 changes: 33 additions & 44 deletions packages/context-control/lib/model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@
// deliberately imperfect — it produces the speckle pattern the heatmap
// exists to expose. Use it to exercise the UI loop.
//
// - databricks: a real LLM behind a Databricks serving endpoint with an
// OpenAI-compatible chat API. This is the model actually under test:
// it sees the ask plus the menu (id, kind, description — exactly the
// - openai-compatible: a real LLM behind an OpenAI-compatible chat API.
// It sees the ask plus the menu (id, kind, description — exactly the
// selection surface `ghost gather` emits) and returns node ids.
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

const STOPWORDS = new Set([
"a",
Expand Down Expand Up @@ -127,52 +122,47 @@ export function parseIdReply(text) {
}
}

/**
* Databricks serving-endpoint adapter (OpenAI-compatible chat API).
* Auth comes from the `databricks` CLI's cached OAuth token; the host
* comes from DATABRICKS_HOST. Nothing is stored.
*/
export function databricksModel({
host = process.env.DATABRICKS_HOST,
endpoint = process.env.CONTEXT_CONTROL_ENDPOINT ?? "goose",
/** OpenAI-compatible chat adapter configured entirely through public env vars. */
export function openAICompatibleModel({
baseUrl = process.env.CONTEXT_CONTROL_BASE_URL,
apiKey = process.env.CONTEXT_CONTROL_API_KEY,
model = process.env.CONTEXT_CONTROL_MODEL,
} = {}) {
if (!host) throw new Error("databricks model needs DATABRICKS_HOST");
let tokenPromise = null;
const getToken = () => {
tokenPromise ??= execFileAsync("databricks", [
"auth",
"token",
"--host",
host,
]).then(({ stdout }) => JSON.parse(stdout).access_token);
return tokenPromise;
};
if (!baseUrl) {
throw new Error("openai-compatible model needs CONTEXT_CONTROL_BASE_URL");
}
if (!apiKey) {
throw new Error("openai-compatible model needs CONTEXT_CONTROL_API_KEY");
}
if (!model) {
throw new Error("openai-compatible model needs CONTEXT_CONTROL_MODEL");
}
return {
name: `databricks:${endpoint}`,
name: "openai-compatible",
async select({ ask, menu, cover }) {
const token = await getToken();
const res = await fetch(
`${host}/serving-endpoints/${endpoint}/invocations`,
`${baseUrl.replace(/\/$/, "")}/chat/completions`,
{
method: "POST",
headers: {
authorization: `Bearer ${token}`,
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model,
messages: [
{ role: "system", content: SELECT_SYSTEM },
{ role: "user", content: selectUser(ask, menu, cover) },
],
// Trial-to-trial variance is the signal being measured, so
// sample at the endpoint's default temperature; do not pin 0.
// Trial-to-trial variance is the signal being measured, so sample at
// the endpoint's default temperature rather than pinning it to zero.
max_tokens: 1024,
}),
},
);
if (!res.ok) {
throw new Error(
`databricks ${endpoint}: ${res.status} ${await res.text()}`,
`openai-compatible model: ${res.status} ${await res.text()}`,
);
}
const data = await res.json();
Expand All @@ -182,14 +172,14 @@ export function databricksModel({
}

const MODEL_ADAPTERS = {
databricks: {
available: () => Boolean(process.env.DATABRICKS_HOST),
create: (name) => {
const endpoint = name.includes(":")
? name.slice(name.indexOf(":") + 1)
: null;
return databricksModel(endpoint ? { endpoint } : {});
},
"openai-compatible": {
available: () =>
Boolean(
process.env.CONTEXT_CONTROL_BASE_URL &&
process.env.CONTEXT_CONTROL_API_KEY &&
process.env.CONTEXT_CONTROL_MODEL,
),
create: () => openAICompatibleModel(),
},
"fake-lexical": {
available: () => true,
Expand All @@ -199,10 +189,9 @@ const MODEL_ADAPTERS = {

/** Resolve a model name exposed by availableModels. */
export function resolveModel(name = availableModels()[0]) {
const key = name?.startsWith("databricks:") ? "databricks" : name;
const adapter = MODEL_ADAPTERS[key];
const adapter = MODEL_ADAPTERS[name];
if (!adapter) throw new Error(`unknown model: ${name}`);
return adapter.create(name);
return adapter.create();
}

/** Available adapter names in default order. */
Expand Down
17 changes: 16 additions & 1 deletion packages/context-control/test/context-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { initFingerprintPackage } from "../../ghost/src/scan/fingerprint-package.js";
import { parseAsks } from "../lib/bench.mjs";
import { parseIdReply } from "../lib/model.mjs";
import { openAICompatibleModel, parseIdReply } from "../lib/model.mjs";
import {
consistency,
jaccard,
Expand Down Expand Up @@ -145,6 +145,21 @@ describe("demo asks", () => {
});
});

describe("openAICompatibleModel", () => {
it("requires portable endpoint configuration", () => {
expect(() => openAICompatibleModel({})).toThrow("CONTEXT_CONTROL_BASE_URL");
expect(() =>
openAICompatibleModel({ baseUrl: "https://models.example.com/v1" }),
).toThrow("CONTEXT_CONTROL_API_KEY");
expect(() =>
openAICompatibleModel({
baseUrl: "https://models.example.com/v1",
apiKey: "test-key",
}),
).toThrow("CONTEXT_CONTROL_MODEL");
});
});

describe("parseIdReply", () => {
it("parses a bare JSON array", () => {
expect(parseIdReply('["a", "b.c"]')).toEqual(["a", "b.c"]);
Expand Down
Loading
Loading