Skip to content
Open
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
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ targets:
- name: npm
id: "@sentry/junior"
includeNames: /^sentry-junior-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-acp"
includeNames: /^sentry-junior-acp-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-plugin-api"
includeNames: /^sentry-junior-plugin-api-\d.*\.tgz$/
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ jobs:
- .github/actions/**
core:
- packages/junior/**
- packages/junior-acp/**
- packages/junior-plugin-api/**
- packages/junior-testing/**
- policies/**
dashboard:
- packages/junior-dashboard/**
- packages/junior/**
- packages/junior-acp/**
- packages/junior-plugin-api/**
docs:
- packages/docs/**
Expand All @@ -65,6 +67,7 @@ jobs:
example:
- apps/example/**
- packages/junior/**
- packages/junior-acp/**
- packages/junior-plugin-api/**
lint_config:
- ast-grep/**
Expand Down Expand Up @@ -347,6 +350,7 @@ jobs:
run: |
mkdir -p artifacts
pnpm --filter @sentry/junior pack --pack-destination artifacts
pnpm --filter @sentry/junior-acp pack --pack-destination artifacts
pnpm --filter @sentry/junior-plugin-api pack --pack-destination artifacts
pnpm --filter @sentry/junior-agent-browser pack --pack-destination artifacts
pnpm --filter @sentry/junior-amplitude pack --pack-destination artifacts
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pnpm build:pkg
This repo uses Craft for manual lockstep npm releases of:

- `@sentry/junior`
- `@sentry/junior-acp`
- `@sentry/junior-plugin-api`
- `@sentry/junior-agent-browser`
- `@sentry/junior-amplitude`
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Start here:
| Package | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `@sentry/junior` | Core Slack bot runtime |
| `@sentry/junior-acp` | Remote ACP transport over Junior Conversations |
| `@sentry/junior-plugin-api` | Lightweight plugin API types and helpers |
| `@sentry/junior-agent-browser` | Agent Browser plugin package for browser automation |
| `@sentry/junior-amplitude` | Read-only Amplitude product analytics through Amplitude's hosted MCP server |
Expand Down
9 changes: 6 additions & 3 deletions ast-grep/rules/no-core-plugin-static-imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ rule:
kind: string
regex: '^["@'']@sentry/junior-.+["@'']$'
- not:
has:
kind: string
regex: '^["@'']@sentry/junior-plugin-api["@'']$'
any:
- has:
kind: string
regex: '^["@'']@sentry/junior-plugin-api["@'']$'
# ACP is an optional transport package. Type imports do not load it.
- pattern: import type $$$A from "@sentry/junior-acp"
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"worktree:setup": "node scripts/worktree.mjs setup",
"cloudflare:token": "node scripts/refresh-cloudflare-tunnel-token.mjs",
"prepare": "simple-git-hooks",
"lint": "pnpm file-length:check && pnpm migration-metadata:check && pnpm test-architecture:check && pnpm dashboard-style:check && pnpm --filter @sentry/junior tool-annotations:check && pnpm --filter @sentry/junior tool-error-classification:check && pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm --filter @sentry/junior-github lint && pnpm --filter @sentry/junior-linear lint && pnpm --filter @sentry/junior-sentry lint && pnpm --filter @sentry/junior-vercel lint && pnpm --filter @sentry/junior-dashboard lint && pnpm ast-grep:lint && pnpm package:lint",
"lint": "pnpm file-length:check && pnpm migration-metadata:check && pnpm test-architecture:check && pnpm dashboard-style:check && pnpm --filter @sentry/junior tool-annotations:check && pnpm --filter @sentry/junior tool-error-classification:check && pnpm --filter @sentry/junior-acp lint && pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm --filter @sentry/junior-github lint && pnpm --filter @sentry/junior-linear lint && pnpm --filter @sentry/junior-sentry lint && pnpm --filter @sentry/junior-vercel lint && pnpm --filter @sentry/junior-dashboard lint && pnpm ast-grep:lint && pnpm package:lint",
"lint:fix": "pnpm --filter @sentry/junior lint:fix",
"file-length:check": "node --test scripts/check-file-length.test.mjs && node scripts/check-file-length.mjs",
"migration-metadata:check": "node --test scripts/check-drizzle-migrations.test.mjs && node scripts/check-drizzle-migrations.mjs",
Expand All @@ -26,7 +26,7 @@
"docs:dev": "pnpm --filter @sentry/junior-docs dev",
"docs:build": "pnpm --filter @sentry/junior-docs build",
"docs:check": "pnpm --filter @sentry/junior-docs check",
"package:lint": "for pkg in packages/junior packages/junior-plugin-api packages/junior-memory packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-amplitude packages/junior-cloudflare packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-octolens packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"package:lint": "for pkg in packages/junior packages/junior-acp packages/junior-plugin-api packages/junior-memory packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-amplitude packages/junior-cloudflare packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-octolens packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"release:check": "node scripts/check-release-config.mjs",
"start": "pnpm --filter @sentry/junior-example dev",
"test": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-linear build && pnpm --filter @sentry/junior-sentry build && pnpm --filter @sentry/junior-vercel build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-sentry test && pnpm --filter @sentry/junior-vercel test && pnpm --filter @sentry/junior-dashboard test",
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/content/docs/contribute/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ related:
Junior uses lockstep package releases for:

- `@sentry/junior`
- `@sentry/junior-acp`
- `@sentry/junior-plugin-api`
- `@sentry/junior-agent-browser`
- `@sentry/junior-amplitude`
Expand Down
14 changes: 9 additions & 5 deletions packages/docs/src/content/docs/reference/config-and-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ import { createApp } from "@sentry/junior";

const app = await createApp({
experimental: {
// ACP v1 Streamable HTTP for one-process development and testing.
// ACP v1 Streamable HTTP backed by the configured shared state.
acp: true,
// Model-facing spawnAgent for durable child agent work. Incomplete; keep off
// unless you are testing the #879 runtime.
Expand All @@ -151,10 +151,14 @@ const app = await createApp({
`junior chat` enables experimental `subagents` automatically because it is the
local createApp-equivalent entrypoint and already wires the child-worker path.

`acp` mounts `GET`, `POST`, and `DELETE /api/acp`. Every request needs a Junior
personal token in the bearer authorization header. The current transport keeps
connection state in one Node process. Use it only for local or single-process
testing. Run `pnpm acp:local` in this repository for a loopback test with the
`acp` mounts `GET`, `POST`, and `DELETE /api/acp`. The client must support ACP
URL elicitation. Junior uses the dashboard Google sign-in flow and rejects
personal tokens on this route. The route stores connection, authorization, and
stream records in the configured `StateAdapter`. The production Redis adapter
lets requests reach different app instances. It does not need process affinity.
The memory adapter remains local to one process. A client must reconnect and
call `session/load` when its live SSE request reaches the deployment request
limit. Run `pnpm acp:local` in this repository for a loopback test with the
official ACP SDK client.

## Install-wide config defaults
Expand Down
12 changes: 7 additions & 5 deletions packages/docs/src/content/docs/reference/handler-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Handled `GET` routes:
- `/api/oauth/callback/:provider`
- `/api/oauth/callback/mcp/:provider`

When `createApp({ dashboard })` mounts `@sentry/junior-dashboard`, the dashboard package owns `/`, `/conversations`, `/conversations/*`, `/locations`, `/locations/*`, `/people`, `/people/*`, `/system`, `/system/*`, `/_junior/dashboard/client.js`, `/auth/login`, `/api/auth/*`, and the authenticated product API routes `/api/health`, `/api/runtime`, `/api/plugins`, `/api/plugins/*`, `/api/plugin-reports`, `/api/skills`, `/api/conversations`, `/api/conversations/*`, `/api/locations`, `/api/locations/*`, `/api/people`, `/api/people/*`, `/api/config`, and `/api/me`; use `/health` for unauthenticated health checks. Plugin API routes are mounted under `/api/plugins/:plugin/*` and inherit auth.
When `createApp({ dashboard })` mounts `@sentry/junior-dashboard`, the dashboard package owns `/`, `/conversations`, `/conversations/*`, `/locations`, `/locations/*`, `/people`, `/people/*`, `/system`, `/system/*`, `/_junior/dashboard/client.js`, `/auth/login`, `/api/auth/*`, `/api/acp/auth/*`, and the authenticated product API routes `/api/health`, `/api/runtime`, `/api/plugins`, `/api/plugins/*`, `/api/plugin-reports`, `/api/skills`, `/api/conversations`, `/api/conversations/*`, `/api/locations`, `/api/locations/*`, `/api/people`, `/api/people/*`, `/api/config`, and `/api/me`; use `/health` for unauthenticated health checks. Plugin API routes are mounted under `/api/plugins/:plugin/*` and inherit auth.

Handled `POST` routes:

Expand All @@ -30,10 +30,12 @@ Handled `POST` routes:
- `/api/webhooks/:platform` (Slack path is `/api/webhooks/slack`)

When `createApp({ experimental: { acp: true } })` is set, `GET`, `POST`, and
`DELETE /api/acp` expose ACP v1 Streamable HTTP. Every request requires a Junior
personal token in the bearer authorization header. This experimental route
keeps connection state in one Node process. Do not enable it on a multi-process
deployment.
`DELETE /api/acp` expose ACP v1 Streamable HTTP. Clients authenticate through
ACP URL elicitation and the dashboard Google sign-in flow. Personal tokens do
not grant access. This experimental route stores transport records in the
configured `StateAdapter`. The production Redis adapter supports requests from
different app instances. Live SSE requests still end at the deployment request
limit. Clients must reconnect and call `session/load` after that limit.

## Expected behavior

Expand Down
91 changes: 91 additions & 0 deletions packages/junior-acp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Remote ACP

Junior exposes ACP v1 Streamable HTTP at `/api/acp` when the app sets
`experimental: { acp: true }`. The route accepts `GET`, `POST`, and `DELETE`.
The client must support ACP URL elicitation. Junior advertises browser sign-in
as its ACP authentication method. The dashboard completes Google OAuth and
binds the verified Junior user to the ACP connection. Personal tokens do not
grant ACP access.

The adapter maps an ACP session to a private Conversation. It uses the existing
web Actor, API Turn mailbox, worker, event store, and Conversation access rules.
Client paths do not select the Junior sandbox. Client MCP servers, resource
links, media, filesystem callbacks, and terminal callbacks are not supported.
`session/cancel` stops the active Turn and returns the ACP `cancelled` stop
reason.

## Package Boundary

`@sentry/junior-acp` owns ACP JSON-RPC, SSE, connection state, and browser
authorization transactions. It does not import Junior core. The package owns a
`ConversationPort` with six Conversation operations. Junior implements that
contract in one adapter module. The adapter owns user access, mailbox admission,
cancellation, and event projection. Core loads the ACP runtime through app setup.
No ACP type enters the agent loop.

## Runtime Design

The ACP `sessionId` is the Junior `conversationId`. The Conversation remains
the source for history, Turn state, and replay. The normal mailbox, queue,
lease, checkpoint, and event paths run the work.

The ACP transport stores hashed connection credentials, browser authorization
transactions, retry receipts, pending stream items, stream cursors, and stream
leases in the Junior `StateAdapter`.
Production uses the existing Redis adapter. Any ACP request can reach any app
instance. The route does not need process affinity or a separate ACP service.
The memory adapter remains process-local and is suitable only for local use.
Each stream preserves up to 1,024 undelivered items. It returns `503` instead
of dropping an item when that limit is full.

Junior admits a prompt only while the Conversation has no runnable work. The
Conversation mutation lock protects this check and the mailbox append. This
prevents retryable work from overlapping a follow-up.

## Live HTTP Limit

ACP v1 delivers server messages over live SSE requests. The official client
opens one connection stream and one stream for each active session. Junior can
restore a Conversation after either stream disconnects, but ACP v1 does not
replay an in-flight transport response on a new connection.

`juniorNitro()` sets the default Vercel function duration to 300 seconds. A
client must create a new ACP connection and call `session/load` after a live
stream reaches that limit. Junior keeps running accepted work after a client
disconnects, and a later load replays its stored Messages. A prompt that is
still in flight when the stream closes cannot resolve transparently in the old
client connection.

ACP remains an opt-in experimental surface. Test client-specific session,
resource, and tool behavior before enabling it for general use.

Run the official-SDK smoke client against a single local process through the
existing tunnel:

```sh
JUNIOR_ACP_URL=https://example.trycloudflare.com/api/acp \
JUNIOR_ACP_FOLLOW_UP="Send one follow-up reply." \
pnpm --filter @sentry/junior acp:smoke
```

The client prints a one-time sign-in URL. Open it in a browser and finish
Google sign-in. The app must mount the authenticated dashboard, and its Google
OAuth client must allow the app's `/api/auth/callback/google` URL.

Set `JUNIOR_ACP_SESSION_ID` to load an earlier Conversation before the first
prompt. The client always reconnects once and loads the active session. It
prints the session id so it can be reused.

## Local Validation

Run `pnpm acp:local` from the repository root. The command starts the local
Postgres and Redis services, applies core migrations, and opens the real
`/api/acp` route on loopback. It runs the official SDK smoke client with two
Turns and one reconnect, and then exits. A local callback route completes
the same ACP authorization transaction without Google. The test server uses
the normal Conversation, mailbox, worker, event, and replay paths. It replaces
only Google sign-in, Vercel Queue transport, and model generation with local
test adapters.

This command is test equipment. It does not add a local ACP transport to the
product. The Compose services stay available for later local tests.
48 changes: 48 additions & 0 deletions packages/junior-acp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@sentry/junior-acp",
"version": "0.172.0",
"private": false,
"publishConfig": {
"access": "public"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/getsentry/junior.git",
"directory": "packages/junior-acp"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
},
"./testing": {
"types": "./src/testing.ts",
"default": "./dist/testing.js"
}
},
"files": [
"dist",
"README.md",
"src"
],
"scripts": {
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"lint": "oxlint --config ../junior/.oxlintrc.json --deny-warnings src tsup.config.ts",
"prepare": "pnpm run build",
"prepack": "pnpm run build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@sentry/junior-plugin-api": "workspace:*",
"chat": "4.29.0",
"zod": "catalog:"
},
"devDependencies": {
"@types/node": "^25.9.1",
"oxlint": "^1.66.0",
"tsup": "^8.5.1",
"typescript": "^6.0.3"
}
}
Loading
Loading