Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 .github/actions/prune-repository/prune.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
],
# The app's condition-5 test executes the evaluation runner as a child
# process; the context root is not a workspace and must be copied explicitly.
"@apps/brunch-agent": ["libs/@hashintel/brunch-agent/evaluations"],
}

TURBO_QUERY = """
Expand Down
8 changes: 8 additions & 0 deletions .github/actions/prune-repository/prune_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ def test_core_job_adds_the_app_and_context_paths(self) -> None:
self.assertEqual(
extra_paths_for_requested({CORE}),
[
".config/oxlint/brunch",
"libs/@hashintel/brunch-agent/AGENTS.md",
"libs/@hashintel/brunch-agent/CONTEXT.md",
"libs/@hashintel/brunch-agent/docs",
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
],
)

def test_app_job_adds_the_baseline_evaluation_paths(self) -> None:
self.assertEqual(
extra_paths_for_requested({APP}),
["libs/@hashintel/brunch-agent/evaluations"],
)

def test_sibling_or_website_job_does_not_add_brunch_extras(self) -> None:
self.assertEqual(extras_for_requested({TRANSPORT}), frozenset())
self.assertEqual(extras_for_requested({WEBSITE}), frozenset())
Expand Down
10 changes: 7 additions & 3 deletions apps/brunch-agent/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"paths": [
{
"name": "@hashintel/petrinaut",
"message": "The Brunch server must remain independent of Petrinaut implementations."
"message": "The Brunch server must remain independent of Petrinaut UI. Import catalogs from @hashintel/petrinaut-core, not the editor package."
}
],
"patterns": [
Expand All @@ -35,8 +35,12 @@
"message": "The Brunch application must not depend on unpublished HASH packages."
},
{
"group": ["@hashintel/petrinaut/*", "@hashintel/petrinaut-*"],
"message": "The Brunch server must remain independent of Petrinaut implementations."
"group": [
"@hashintel/petrinaut/*",
"@hashintel/petrinaut-cli",
"@hashintel/petrinaut-cli/*"
],
"message": "The Brunch server must remain independent of Petrinaut UI and CLI. Import catalogs from @hashintel/petrinaut-core, not the editor package."
}
]
}
Expand Down
14 changes: 9 additions & 5 deletions apps/brunch-agent/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Brunch agent application

This application belongs to the Brunch context rooted at
`../../libs/@hashintel/brunch-agent/`. Read that context's `AGENTS.md`, `CONTEXT.md`, and relevant
ADRs before changing this application. HASH root guidance takes precedence.
`../../libs/@hashintel/brunch-agent/`. Read that context's `AGENTS.md` and current `MISSION.md`
before changing this application. If `MISSION.next.md` exists, it is a scratchpad for later
concerns and is not execution authority. Consult `CONTEXT.md` or historical design documents only when a
concrete question requires them; ADRs and specs are hypotheses, not implementation obligations.
HASH root guidance takes precedence.

The application composes the Brunch packages, Flue runtime, HTTP routes, and local diagnostics. It
must remain independent of Petrinaut implementation packages; `apps/petrinaut-website` meets it
through the AI SDK/HTTP transport.
The application composes the Flue runtime, HTTP routes, and the Brunch packages required by the
current mission. It must remain independent of Petrinaut UI (`@hashintel/petrinaut`); it may import
published catalogs from `@hashintel/petrinaut-core` (for example user-guide page ids the panel
already executes). `apps/petrinaut-website` meets the editor through the AI SDK/HTTP transport.
50 changes: 50 additions & 0 deletions apps/brunch-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Brunch agent application

## Run the Petrinaut panel locally

From the repository root, make `ANTHROPIC_API_KEY` available in the environment and run:

```sh
yarn dev:brunch
```

The first step builds the Petrinaut libraries the panel imports (`dist/` and design-system
codegen). Then it starts the Brunch server at `http://127.0.0.1:4321` and the real Petrinaut
website at `http://127.0.0.1:4915`. The website proxies `/api/chat` to Brunch. The panel talks to one plain
Flue chat agent: streamed text and reasoning, one server `ping` tool, one stub
skill (`confirm-path`, activated via `activate_skill`), and the existing Petrinaut
`readPetrinautDoc` client tool. There is no elicitation loop, sweep tool, or
`brunch_ask` on this path. Capture is a harness-side pipe: an explicit settled
range of Flue history is applied into a JSON store beside the conversation
database, not by the interviewer.

Conversations persist in `apps/brunch-agent/.data-wipe-me/conversations.db`. `BRUNCH_DEV_DB_PATH`
overrides that local path. Capture envelopes for one Flue conversation sit beside that sqlite
file, named by the hashed instance id (`<instanceId>.json`). The hermetic `/api/chat` test uses
`BRUNCH_CHAT_DB_PATH` and writes the capture file in that same directory. Flue history is the
conversation log; the capture store is not a second transcript. The browser may cache messages
but reload hydrates from `GET /api/chat?id=`.

The mounted Flue URL `/agents/chat/:id` requires the same principal and conversation identity (`x-brunch-principal` and `x-brunch-conversation`) as `/api/chat`; the path id is the hash of those, not a bearer token.

Print a human-readable transcript of one conversation from that same Flue history (server already
running):

```sh
yarn workspace @apps/brunch-agent transcript -- --principal <key> --id <conversationId>
```

## Voice dock

A second input modality joins the same chat door. It is not a voice route and does not own
provider audio or session state.

| | |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| URL | `POST /api/chat` (and `GET /api/chat?id=` to hydrate) |
| Identity | `x-brunch-principal` plus body `id` (the conversation id). The server hashes those into the Flue instance id. |
| Initial turn | JSON `{ id, trigger: "submit-message", messages }` whose last user text part is the utterance. |
| Client-tool follow-up | Same POST, with `messageId` of the assistant message and completed client-tool parts (`providerExecuted` not true). Correlated by `toolCallId`. |
| Response | AI SDK UI-message stream (SSE). |

`OPTIONS /api/chat` is the CORS preflight for that same contract.
2 changes: 1 addition & 1 deletion apps/brunch-agent/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Elicitation harness β€” dev app</title>
<title>Brunch Flue chat</title>
</head>
<body>
<div id="root"></div>
Expand Down
11 changes: 6 additions & 5 deletions apps/brunch-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@apps/brunch-agent",
"version": "0.0.0-private",
"private": true,
"description": "Remote Brunch server, local development loop, target gallery, and diagnostic probe surface.",
"description": "Remote Brunch server, local development loop, and Petrinaut /api/chat door.",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
Expand All @@ -12,17 +12,18 @@
"lint:eslint": "oxlint --type-aware --type-check --report-unused-disable-directives-severity=error .",
"lint:tsc": "tsgo --noEmit",
"petrinaut:dev": "vite dev --config petrinaut-local.vite.config.ts",
"test:unit": "vitest run --config vitest.config.ts"
"test:unit": "vitest run --config vitest.config.ts",
"transcript": "node --experimental-strip-types src/transcript-cli.ts"
},
"dependencies": {
"@flue/opentelemetry": "2.0.3",
"@flue/react": "2.0.3",
"@flue/runtime": "2.0.3",
"@flue/sdk": "2.0.3",
"@hashintel/brunch-agent": "workspace:*",
"@hashintel/brunch-agent-binding-flue": "workspace:*",
"@hashintel/brunch-agent-plugin-gherkin": "workspace:*",
"@hashintel/brunch-agent-plugin-sdcpn": "workspace:*",
"@hashintel/brunch-agent-transport-aisdk": "workspace:*",
"@hashintel/petrinaut-core": "workspace:*",
"@opentelemetry/api": "1.9.1",
"hono": "4.13.2",
"react": "19.2.6",
"react-dom": "19.2.6",
Expand Down
4 changes: 1 addition & 3 deletions apps/brunch-agent/petrinaut-local.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ const withoutIncumbentChatHandler = (
export default defineConfig(async (environment) => {
const websiteRoot = process.env.PETRINAUT_WEBSITE_ROOT;
if (!websiteRoot) {
throw new Error(
"PETRINAUT_WEBSITE_ROOT must point at hash/apps/petrinaut-website for the real-panel run.",
);
throw new Error("PETRINAUT_WEBSITE_ROOT is required.");
}
const root = resolve(websiteRoot);
// Babel resolves the React compiler plugin from the launched project's cwd,
Expand Down
36 changes: 36 additions & 0 deletions apps/brunch-agent/src/agent-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** Hono middleware: the mounted Flue route enforces the same ownership rule as `/api/chat`. */

import { BRUNCH_PRINCIPAL_HEADER } from "@hashintel/brunch-agent-transport-aisdk/headers";

import { ownsFlueInstance } from "./conversation-identity.ts";
import { BRUNCH_CONVERSATION_HEADER } from "./conversation-payload.ts";

import type { MiddlewareHandler } from "hono";

export const agentOwnershipGuard = (mountPrefix: string): MiddlewareHandler => {
return async (context, next) => {
const principalKey = context.req.header(BRUNCH_PRINCIPAL_HEADER)?.trim();
const conversationId = context.req
.header(BRUNCH_CONVERSATION_HEADER)
?.trim();
if (
principalKey === undefined ||
principalKey.length === 0 ||
conversationId === undefined ||
conversationId.length === 0
) {
return context.json({ error: "unauthorized" }, 401);
}
const instanceId = context.req.path
.slice(mountPrefix.length)
.split("/")
.find((segment) => segment.length > 0);
if (
instanceId === undefined ||
!ownsFlueInstance(principalKey, conversationId, instanceId)
) {
return context.json({ error: "forbidden" }, 403);
}
return next();
};
};
46 changes: 46 additions & 0 deletions apps/brunch-agent/src/agents/chat-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use agent";
/**
* One plain Flue chat agent for the Petrinaut panel throughline.
*
* Capture is a harness-side pipe, not an interviewer tool. One stub skill is
* mounted so activation can appear in Flue history.
*/

import { defineSkill, useModel, useSkill, useTool } from "@flue/runtime";

import { ping } from "../tools/ping.ts";
import { readPetrinautDoc } from "../tools/read-petrinaut-doc.ts";

export const CHAT_MODEL_ID =
process.env["BRUNCH_CHAT_MODEL"] || "claude-haiku-4-5";

export const STUB_SKILL_NAME = "confirm-path";

export const ACTIVATE_SKILL_TOOL_NAME = "activate_skill";

const confirmPath = defineSkill({
name: STUB_SKILL_NAME,
description:
"Confirm how this assistant is mounted. Use when checking the server path or tool layout.",
instructions:
"Say that ping confirms the server tool path. Then continue helping the user.",
});

export function ChatAgent() {
useModel(`anthropic/${CHAT_MODEL_ID}`);
useSkill(confirmPath);
useTool(ping);
useTool(readPetrinautDoc);
return [
"You are a concise assistant inside the Petrinaut editor.",
"Call ping when you need to confirm the server tool path.",
`Activate the \`${STUB_SKILL_NAME}\` skill before calling ping.`,
"When the user asks how Petrinaut's UI works, call readPetrinautDoc.",
"A client-tool-result signal is JSON [{ toolCallId, toolName, output }]. Treat output as the browser's result for that call and continue helping the user.",
].join("\n");
}

/**
* Pinned, and never to be edited: conversation storage keys on this literal.
*/
ChatAgent.agentName = "brunch-chat-agent";
76 changes: 0 additions & 76 deletions apps/brunch-agent/src/agents/gherkin-elicitor.ts

This file was deleted.

54 changes: 0 additions & 54 deletions apps/brunch-agent/src/agents/sdcpn-elicitor.ts

This file was deleted.

Loading
Loading