Skip to content
Draft
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
37 changes: 37 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# AppKit Data Contracts

Proto definitions that are the single source of truth for the interfaces
between AppKit components: TS/Python/Rust backends, the React SDK, the
`databricks` CLI, the agent-eval framework, and scaffolding tooling.

See the design doc (linked from `docs/docs/contracts.md`) for the full
five-contract plan. This directory ships the first slice of that plan —
`appkit/v1/wire.proto` — as an intro.

## Layout

```
contracts/
buf.yaml # buf module + lint/breaking rules
buf.gen.yaml # codegen config (TS via @bufbuild/protoc-gen-es)
appkit/v1/
wire.proto # HTTP + SSE envelope, error frames, result-format enum
```

## Regenerating TS bindings

```bash
pnpm --filter=@databricks/appkit-contracts generate
```

Generated output lands in `packages/contracts/src/generated/` and is re-exported
from `@databricks/appkit-contracts`.

## Follow-ups

Planned additions, tracked against the design doc:

- `appkit/v1/query.proto` — typed SQL query schemas (DESCRIBE QUERY -> bindings).
- `appkit/v1/plugin.proto` — plugin manifest, replacing the JSON Schema in `packages/shared/src/schemas/plugin-manifest.schema.json`.
- `agenteval/v1/*.proto` — eval runs, scorers, traces, streaming envelope.
- `appkit/v1/deploy.proto` — app manifest consumed by `databricks apps deploy`.
59 changes: 59 additions & 0 deletions contracts/appkit/v1/wire.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Wire-protocol contract for AppKit (draft, v1).
//
// This proto defines the HTTP + SSE surface every AppKit backend must serve.
// It is the single source of truth for the TS, Python, and Rust ports,
// and for the React SDK that consumes the stream.
//
// Scope of this initial draft is intentionally small: envelope shape,
// structured error frames, and SQL warehouse result-format negotiation.
// Route conventions, heartbeat/reconnect semantics, and the client-config
// payload will land in follow-up PRs.
syntax = "proto3";

package appkit.v1;

// Envelope for a single Server-Sent Event frame emitted by an AppKit backend
// and parsed by the React SDK via connectSSE(). Structurally compatible with
// the current BufferedEvent type in packages/appkit/src/stream/types.ts.
message SseEnvelope {
// Monotonically increasing stream event identifier. Clients echo the most
// recent id back via the Last-Event-ID header to resume a stream.
string id = 1;

// Event type. Opaque to the envelope; interpreted per-plugin.
string event = 2;

// UTF-8 payload. Binary content should be base64-encoded by the emitter.
string data = 3;

// Emission timestamp in milliseconds since epoch (wall clock on the emitter).
int64 timestamp_ms = 4;
}

// Structured error frame. Emitted over the same SSE transport as SseEnvelope
// but distinguished by event="error".
message SseError {
SseErrorCode code = 1;
string message = 2;
}

enum SseErrorCode {
SSE_ERROR_CODE_UNSPECIFIED = 0;
SSE_ERROR_CODE_TEMPORARY_UNAVAILABLE = 1;
SSE_ERROR_CODE_TIMEOUT = 2;
SSE_ERROR_CODE_INTERNAL_ERROR = 3;
SSE_ERROR_CODE_INVALID_REQUEST = 4;
SSE_ERROR_CODE_STREAM_ABORTED = 5;
SSE_ERROR_CODE_STREAM_EVICTED = 6;
SSE_ERROR_CODE_UPSTREAM_ERROR = 7;
}

// SQL warehouse result-format negotiation. Backends attempt formats in the
// order specified by their fallback chain — the canonical TS chain today is
// ARROW_STREAM -> JSON -> ARROW.
enum ResultFormat {
RESULT_FORMAT_UNSPECIFIED = 0;
RESULT_FORMAT_ARROW_STREAM = 1;
RESULT_FORMAT_JSON = 2;
RESULT_FORMAT_ARROW = 3;
}
8 changes: 8 additions & 0 deletions contracts/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: v2
clean: true
plugins:
- local: ../node_modules/.bin/protoc-gen-es
out: ../packages/contracts/src/generated
opt:
- target=ts
- import_extension=js
10 changes: 10 additions & 0 deletions contracts/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: v2
modules:
- path: .
name: buf.build/databricks/appkit
lint:
use:
- STANDARD
breaking:
use:
- FILE
54 changes: 54 additions & 0 deletions docs/docs/contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Data Contracts
sidebar_position: 9
---

# Data Contracts

AppKit exposes several interfaces that today are kept in sync by convention:
the HTTP + SSE surface between backend and React SDK, the typed query pipeline
(`DESCRIBE QUERY` → `QueryRegistry`), the plugin manifest schema, and the app
contract the `databricks` CLI consumes at deploy time. As AppKit grows second
and third backend implementations (Python, Rust) the cost of keeping those
conventions in sync grows with the language count.

The `contracts/` directory at the repo root holds the proto definitions that
will become the single source of truth for all of these interfaces. Each
language binding is regenerated from the same protos; consumers import what
they need.

## Current scope

This intro PR ships the first slice:

- **`appkit/v1/wire.proto`** — the HTTP + SSE envelope. Defines `SseEnvelope`,
`SseError`, and `ResultFormat` (warehouse result-format negotiation).
Consumed today by `packages/appkit/src/stream/types.ts` as a type-level
compatibility anchor; follow-up PRs will retire the hand-written
`BufferedEvent` in favor of the generated type.

## Regenerating

```bash
pnpm contracts:lint # buf lint over contracts/
pnpm contracts:generate # regenerate TS bindings into packages/contracts/src/generated/
```

Generated output lives under `packages/contracts/src/generated/` and is
re-exported from `@databricks/appkit-contracts`.

## Planned follow-ups

Per the design doc (see [Enforcing Data Contracts in
AppKit](https://docs.google.com/document/d/1yWWt7sLVpmuDhYs-bIWFEE9hiI6MlxjO6YS4AuN4v6k/edit)),
four more contracts will land in subsequent PRs:

- `appkit/v1/query.proto` — typed SQL query schemas (`DESCRIBE QUERY` →
bindings in every language, replacing today's TS-only
`QueryRegistry` augmentation).
- `appkit/v1/plugin.proto` — plugin manifest, replacing
`packages/shared/src/schemas/plugin-manifest.schema.json`.
- `agenteval/v1/*.proto` — versioned eval framework ↔ app protocol.
- `appkit/v1/deploy.proto` — app manifest consumed by
`databricks apps deploy` (runtime, entrypoint, build steps, static-asset
layout, health route, required env).
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"ignoreWorkspaces": [
"packages/shared",
"packages/lakebase",
"packages/contracts",
"apps/**",
"docs"
],
Expand Down
1 change: 1 addition & 0 deletions packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"dependencies": {
"@ast-grep/napi": "0.37.0",
"@databricks/appkit-contracts": "workspace:*",
"@databricks/lakebase": "workspace:*",
"@databricks/sdk-experimental": "0.16.0",
"@opentelemetry/api": "1.9.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/appkit/src/stream/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SseEnvelope } from "@databricks/appkit-contracts";
import type { Context } from "@opentelemetry/api";
import type { IAppResponse } from "shared";
import type { EventRingBuffer } from "./buffers";
Expand Down Expand Up @@ -33,6 +34,15 @@ export interface BufferedEvent {
timestamp: number;
}

// Compile-time anchor against contracts/appkit/v1/wire.proto. When BufferedEvent
// and the generated SseEnvelope diverge, tsc fails here — a signal that either
// the contract or the implementation needs updating. Follow-up PRs will retire
// BufferedEvent in favour of the generated type.
type _BufferedEventSseEnvelopeCompat = {
id: BufferedEvent["id"] extends SseEnvelope["id"] ? true : never;
data: BufferedEvent["data"] extends SseEnvelope["data"] ? true : never;
};

export interface StreamEntry {
streamId: string;
generator: AsyncGenerator<any, void, unknown>;
Expand Down
3 changes: 2 additions & 1 deletion packages/appkit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"@/*": ["src/*"],
"@tools/*": ["../../tools/*"],
"shared": ["../../packages/shared/src"],
"@databricks/lakebase": ["../../packages/lakebase/src"]
"@databricks/lakebase": ["../../packages/lakebase/src"],
"@databricks/appkit-contracts": ["../../packages/contracts/src"]
}
},
"include": ["src/**/*"],
Expand Down
36 changes: 36 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@databricks/appkit-contracts",
"type": "module",
"version": "0.0.1",
"private": true,
"description": "Shared proto data contracts for AppKit (wire, query, plugin, agenteval, deploy). Single source of truth for cross-language interface shapes.",
"exports": {
".": {
"development": "./src/index.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"generate": "tsx ../../tools/generate-contracts.ts",
"build:package": "pnpm run generate && tsdown --config tsdown.config.ts",
"build:watch": "tsdown --config tsdown.config.ts --watch",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist",
"clean:full": "rm -rf dist node_modules src/generated"
},
"dependencies": {
"@bufbuild/protobuf": "2.11.0"
},
"devDependencies": {
"@bufbuild/buf": "1.47.2",
"@bufbuild/protoc-gen-es": "2.11.0"
},
"types": "./dist/index.d.ts",
"publishConfig": {
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
}
}
}
Loading
Loading