Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b4c1775
Keep Petrinaut's diagnostics context out of Brunch's user turns
lunelson Oct 15, 2026
6356a27
Answer query_basis from the reflections that name a net element
lunelson Oct 15, 2026
5e7e9df
Read the Ledger tab as a document that explains the canvas selection
lunelson Oct 15, 2026
eb6ddfd
Render Ledger Markdown only for the model, and badge the Ledger tab o…
lunelson Oct 15, 2026
5253101
Make the Ledger's trace of the canvas selection stand out
lunelson Oct 16, 2026
b8d5f11
Give net element id matching one home
lunelson Oct 16, 2026
d65af4a
Keep in the Brunch library only what the server and browser must agre…
lunelson Oct 16, 2026
fb92e03
Name the browser-tool envelope once in the Brunch library
lunelson Oct 16, 2026
1c91d1f
Own the browser-call protocol and the browser-executed tools in the B…
lunelson Oct 16, 2026
957a8f2
Type the net read-back as the projected net structure
lunelson Oct 16, 2026
f6ee918
Name the task tool and the net-definition read in the Brunch tool ref…
lunelson Oct 16, 2026
81d5369
Derive Ledger record types and arc directions from their owners
lunelson Oct 16, 2026
2ff09d4
Take server thinking levels, JSON, chronology and identity types from…
lunelson Oct 16, 2026
3e92dce
Project Brunch website voice, approval and replay types from their ow…
lunelson Oct 16, 2026
1ff0685
Export from the Brunch library only what its consumers import
lunelson Oct 16, 2026
8dae3f2
Have query_basis answers quote the turns they rest on
lunelson Oct 19, 2026
f3bfb82
Keep query_basis answers from inventing a rationale when no basis was…
lunelson Oct 19, 2026
15956b2
Keep a reflection under its entity after the claim it cites is replaced
lunelson Oct 19, 2026
c1c022f
Fade the Ledger for a traced selection only when a traced record is s…
lunelson Oct 19, 2026
5e147cf
Identify arcs in query_basis by Petrinaut's generated arc ID
lunelson Oct 20, 2026
2b7941a
Name the claim that stands now for a traced claim replaced more than …
lunelson Oct 20, 2026
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 apps/brunch-agent/docs/petrinaut/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Prompts are imported with `?raw`. Each skill is an Agent Skills directory import

## The shared library

`@hashintel/brunch-agent` holds only what both apps need: Brunch's named constants, conversation identity, the browser-tool envelope and message metadata, the Petrinaut tool contracts (tool effects, net readers, the draft schemas, contextual user messages, initial data) and the Ledger. It has one entry and is browser-safe: it never imports the Flue runtime, which `test/architecture/import-direction.test.ts` enforces. Flue tool definitions, guidance and server mechanics live in the app.
`@hashintel/brunch-agent` holds only what both apps need: Brunch's named constants, conversation identity, the browser-tool envelope and message metadata, the Petrinaut tool contracts (tool effects, net readers, the draft schemas, initial data) and the Ledger. It has one entry and is browser-safe: it never imports the Flue runtime, which `test/architecture/import-direction.test.ts` enforces. Flue tool definitions, guidance and server mechanics live in the app.

## Loading workspace source in dev

Expand All @@ -26,7 +26,7 @@ The Ledger is the ordered sequence of accepted `ledger_commit` calls in the conv

A running commit reads history, folds the accepted commits before its own call and decides from them alone, so an interrupted durable call re-runs to the same IDs. A commit counts only when its recorded output agrees with the IDs its input yields. Flue folds a response's steps into one message and may run a proposal's calls concurrently: a commit that runs while an earlier commit in the same response is unsettled is refused. Its receipt returns the IDs, how many user messages have passed, how many questions are open, the obligations still owed and notes on consequences to weigh.

`ledger_compile` renders the folded Ledger in the agent skin, which carries record IDs and kind tokens inline. The website's Ledger tab renders the same projection in the user skin, and persona evidence (`ledger.md`, `ledger.json`) uses the agent skin.
`ledger_compile` renders the folded Ledger's projection as Markdown, with record IDs and kind tokens inline; persona evidence (`ledger.md`, `ledger.json`) uses the same Markdown. The website's Ledger tab renders the projection as a document, without IDs, marking each record occurrence with its address so a canvas selection can highlight the records `traceLedger` reaches.

## Browser and conversation boundary

Expand All @@ -38,7 +38,7 @@ Flue's persisted conversation is canonical history. In a document-bound conversa

Before each provider request the context projection records which net reads and settled changes the model's context holds. A document change is issued only against the revision of the latest of those; with none left, for example after compaction, it is refused before reaching the browser. The browser accepts the change only if its document has moved since that revision through Brunch's own settled changes, following their before-to-after chain, so a hand edit refuses it as stale. The projection strips the host envelope from browser results and shows the latest change's `readBack` as `netAfterChanges` unless a later net read supersedes it. It does not change canonical conversation history, public history, call identity or ordering.

The server credits an applied canonical call with its settled `after` revision; it does not re-hash or replay browser-produced evidence. `src/shared/conversation/net-changes.ts` projects those calls in history order. `query_basis` looks up a named element in the latest net result that shows the net, a read or a change carrying its `readBack` (`src/shared/conversation/net-view.ts` holds that rule for the projection, net freshness and `query_basis` alike), and lists applied calls associated by element ID, each with the Ledger revision current at the call and the content of the records committed in its turn, before or after the call, since a change's reflections are committed after it. These temporal associations do not prove a semantic basis. A reviewed experiment draft requires an earlier complete net read; the browser prepares against the current document revision. Canonical `createExperiment` remains Petrinaut-owned and does not mutate the document.
The server credits an applied canonical call with its settled `after` revision; it does not re-hash or replay browser-produced evidence. `src/shared/conversation/net-changes.ts` projects those calls in history order. `query_basis` looks up a named element in the latest net result that shows the net, a read or a change carrying its `readBack` (`src/shared/conversation/net-view.ts` holds that rule for the projection, net freshness and `query_basis` alike), and returns its recorded basis: `traceLedger` in `@hashintel/brunch-agent` walks back from the reflections that name the element to the claims, entities and discharged obligations they rest on, and to the turns those records came from, each turn with the opening of the user's words. A traced claim a later claim replaced is reported with its replacement. An element no reflection names has no recorded basis, and the tool says so instead of offering records that merely share its turn. It also lists the applied calls associated by element ID, each with the Ledger revision current at the call. A reviewed experiment draft requires an earlier complete net read; the browser prepares against the current document revision. Canonical `createExperiment` remains Petrinaut-owned and does not mutate the document.

## Runtime and persistence

Expand Down
15 changes: 9 additions & 6 deletions apps/brunch-agent/evals/petrinaut/persona/browser-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import {
type Response as BrowserResponse,
} from "@playwright/test";

import { brunchHeaders, brunchRoutes } from "@hashintel/brunch-agent";
import {
agentOwnershipHeaders,
brunchHeaders,
brunchRoutes,
} from "@hashintel/brunch-agent";

import { agentOwnershipHeaders } from "../../../src/shared/conversation/identity.ts";
import type { ConversationIdentity } from "@hashintel/brunch-agent";

/** A failed persona turn that still carries the admitted conversation's history. */
export class PersonaBrowserTurnError extends Error {
Expand All @@ -29,13 +33,12 @@ export class PersonaBrowserTurnError extends Error {
}
}

export interface PersonaBrowserSession {
/** `initialData` stays as the browser sent it; resume parses it with `sdcpnInitialDataSchema`. */
export type PersonaBrowserSession = ConversationIdentity & {
readonly url: string;
readonly principalKey: string;
readonly conversationId: string;
readonly uid: string;
readonly initialData: unknown;
}
};

/** A destructive edit the harness allowed, described as the approval panel listed it. */
export interface PersonaAllowedEdit {
Expand Down
6 changes: 3 additions & 3 deletions apps/brunch-agent/evals/petrinaut/persona/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DEFAULT_CHAT_MODEL,
DEFAULT_CHAT_THINKING,
} from "../../../src/shared/chat-model.ts";
import { flueConversationIdFrom } from "../../../src/shared/conversation/identity.ts";
import { flueConversationId } from "../../../src/shared/conversation/identity.ts";
import {
documentIdFromInitialData,
personaEnvironment,
Expand Down Expand Up @@ -167,7 +167,7 @@ test.each([false, true])(
JSON.stringify({
...identity,
uid: "TEST-uid",
url: `${config.panelOrigin}/agents/petrinaut/${flueConversationIdFrom(identity)}`,
url: `${config.panelOrigin}/agents/petrinaut/${flueConversationId(identity)}`,
initialData: {
binding: {
conversationId: identity.conversationId,
Expand Down Expand Up @@ -270,7 +270,7 @@ test("resume requires the bridge log that Pi-era runs lack", async () => {
JSON.stringify({
...identity,
uid: "TEST-uid",
url: `${panelOrigin}/agents/petrinaut/${flueConversationIdFrom(identity)}`,
url: `${panelOrigin}/agents/petrinaut/${flueConversationId(identity)}`,
initialData: {
binding: {
conversationId: identity.conversationId,
Expand Down
3 changes: 1 addition & 2 deletions apps/brunch-agent/evals/petrinaut/persona/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { createFlueClient, FlueExecutionError } from "@flue/sdk";
import { chromium, type Page } from "@playwright/test";
import { loadEnv } from "vite";

import { brunchEnv } from "@hashintel/brunch-agent";
import { agentOwnershipHeaders, brunchEnv } from "@hashintel/brunch-agent";
import { parseSDCPNFile, toPetrinautId } from "@hashintel/petrinaut-core";

import {
Expand All @@ -31,7 +31,6 @@ import {
DEFAULT_CHAT_MODEL,
DEFAULT_CHAT_THINKING,
} from "../../../src/shared/chat-model.ts";
import { agentOwnershipHeaders } from "../../../src/shared/conversation/identity.ts";
import { writeRunPacket } from "../run-packet.ts";
import { openPersonaBrowserBridge } from "./browser-bridge.ts";
import {
Expand Down
10 changes: 5 additions & 5 deletions apps/brunch-agent/evals/petrinaut/persona/launch/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { join, resolve } from "node:path";
import { createFlueClient } from "@flue/sdk";
import * as v from "valibot";

import { sdcpnInitialDataSchema } from "@hashintel/brunch-agent";

import {
agentOwnershipHeaders,
flueConversationIdFrom,
} from "../../../../src/shared/conversation/identity.ts";
sdcpnInitialDataSchema,
} from "@hashintel/brunch-agent";

import { flueConversationId } from "../../../../src/shared/conversation/identity.ts";
import { lastAdmittedUtterance } from "./bridge-log.ts";
import { roleSettingsFromRun } from "./role-settings.ts";

Expand Down Expand Up @@ -73,7 +73,7 @@ export const readPersonaResume = async (directory: string) => {
assert.equal(resolve(config.databasePath), join(run, "conversation.db"));
assert.equal(
session.url,
`${config.panelOrigin}/agents/petrinaut/${flueConversationIdFrom(session)}`,
`${config.panelOrigin}/agents/petrinaut/${flueConversationId(session)}`,
);
const binding = session.initialData.binding;
assert.equal(binding.conversationId, session.conversationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
DEFAULT_CHAT_MODEL,
DEFAULT_CHAT_THINKING,
STEP_A_MODEL_ID,
type ChatThinkingLevel,
} from "../../../../src/shared/chat-model.ts";

import type { ThinkingLevel } from "@flue/runtime";

/** Brunch's own model; the persona agent chooses its model through its harness. */
export type PersonaRoleSettings = {
brunchModel: string;
brunchThinking: ChatThinkingLevel;
brunchThinking: ThinkingLevel;
};

const catalog = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export const ledgerEvidence = (history: LedgerHistory): LedgerEvidence => {
const { state, commits } = foldCommits(history);
return {
commits,
markdown: renderLedgerMarkdown(projectLedger(state), "agent"),
markdown: renderLedgerMarkdown(projectLedger(state)),
};
};
2 changes: 1 addition & 1 deletion apps/brunch-agent/evals/petrinaut/persona/rpc-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const personaRecordSchema = v.union([
personaResponseSchema,
personaEventSchema,
]);
export type PersonaRecord = PersonaResponse | PersonaEvent;
export type PersonaRecord = v.InferOutput<typeof personaRecordSchema>;

export type PersonaTranscriptEntry = {
readonly speaker: "user" | "brunch";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ const finalNetOf = (net: unknown): FinalNetSummary | null => {
const isHistoryMessage = (message: unknown): message is LedgerHistoryMessage =>
isRecord(message) &&
typeof message.id === "string" &&
typeof message.role === "string" &&
(message.role === "user" ||
message.role === "assistant" ||
message.role === "system") &&
Array.isArray(message.parts);

const finalLedgerOf = (history: unknown): FinalLedgerSummary | null => {
Expand Down
30 changes: 14 additions & 16 deletions apps/brunch-agent/evals/petrinaut/run-packet/tool-categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ import {
netReaderToolNames,
petrinautToolEffects,
} from "@hashintel/brunch-agent";
import {
createExperimentToolName,
getLatestNetDefinitionToolName,
getNetCompilationErrorsToolName,
readPetrinautDocToolName,
} from "@hashintel/petrinaut-core/ai";

/** What a tool call spends a step on, for turn-time accounting. */
export type ToolCategory =
| "ledger"
| "skills"
| "net-read"
| "compile-check"
| "layout"
| "net-change"
| "experiment"
| "other";

export const toolCategories: readonly ToolCategory[] = [
export const toolCategories = [
"ledger",
"skills",
"net-read",
Expand All @@ -24,11 +20,13 @@ export const toolCategories: readonly ToolCategory[] = [
"net-change",
"experiment",
"other",
];
] as const;

export type ToolCategory = (typeof toolCategories)[number];

const netReadTools = new Set<string>([
"getLatestNetDefinition",
"readPetrinautDoc",
getLatestNetDefinitionToolName,
readPetrinautDocToolName,
...Object.values(netReaderToolNames),
]);

Expand All @@ -54,11 +52,11 @@ export const toolCategoryOf = (toolName: string): ToolCategory => {
return "skills";
}
if (netReadTools.has(toolName)) return "net-read";
if (toolName === "getNetCompilationErrors") return "compile-check";
if (toolName === getNetCompilationErrorsToolName) return "compile-check";
if (layoutTools.has(toolName)) return "layout";
if (
toolName === brunchTools.draftPetrinautExperiment ||
toolName === "createExperiment"
toolName === createExperimentToolName
) {
return "experiment";
}
Expand Down
12 changes: 4 additions & 8 deletions apps/brunch-agent/evals/petrinaut/turn-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fileURLToPath } from "node:url";
import { createFlueClient } from "@flue/sdk";

import {
agentOwnershipHeaders,
brunchEnv,
foldCommits,
projectLedger,
Expand All @@ -27,10 +28,7 @@ import {
DEFAULT_CHAT_MODEL,
DEFAULT_CHAT_THINKING,
} from "../../src/shared/chat-model.ts";
import {
agentOwnershipHeaders,
flueConversationIdFrom,
} from "../../src/shared/conversation/identity.ts";
import { flueConversationId } from "../../src/shared/conversation/identity.ts";
import { loadBuiltBrunchApplication } from "../../test/load-built-application.ts";
import { writeRunPacket } from "./run-packet.ts";

Expand Down Expand Up @@ -72,7 +70,7 @@ try {
conversationId: `probe-${randomUUID()}`,
};
const client = createFlueClient({
url: `http://brunch.local/agents/petrinaut/${flueConversationIdFrom(identity)}`,
url: `http://brunch.local/agents/petrinaut/${flueConversationId(identity)}`,
headers: agentOwnershipHeaders(identity),
fetch: async (input, init) =>
application.fetch(
Expand Down Expand Up @@ -116,9 +114,7 @@ try {
`ledger: ${commits.length} accepted commits, ${state.entities.length} entities, ${state.claims.length} claims, ${state.obligations.length} obligations`,
);
report(`\n--- reply ---\n${reply}`);
report(
`\n--- ledger (agent skin) ---\n${renderLedgerMarkdown(projectLedger(state), "agent")}`,
);
report(`\n--- ledger ---\n${renderLedgerMarkdown(projectLedger(state))}`);
report(`\nhistory: ${join(directory, "history.json")}`);
} finally {
await application.stop();
Expand Down
3 changes: 1 addition & 2 deletions apps/brunch-agent/src/agents/petrinaut/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function PetrinautAgent({ id }: AgentProps) {
: undefined;
if (expected && "refusal" in expected)
throw new Error(expected.refusal);
const result = await issueBrowserCall({
return issueBrowserCall({
instanceId: id,
toolCallId,
toolName,
Expand All @@ -114,7 +114,6 @@ export function PetrinautAgent({ id }: AgentProps) {
: { expectedRevision: expected.revision }),
signal,
});
return { output: result.output, metadata: result.metadata };
},
}
: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { brunchTools, sdcpnInitialDataSchema } from "@hashintel/brunch-agent";
import { petrinautAiTools } from "@hashintel/petrinaut-core/ai";

import {
assertBrowserExecutedToolConformance,
assertPetrinautToolCatalogueConformance,
canonicalPetrinautToolCatalogue,
brunchToolCatalogue,
Expand Down Expand Up @@ -146,3 +147,14 @@ test("the Brunch catalogue classifies every canonical tool", () => {
]),
).toThrow(/unclassified/u);
});

test("the catalogue's website-executed tools are the library's browser-executed tools", () => {
expect(() => assertBrowserExecutedToolConformance()).not.toThrow();
expect(() =>
assertBrowserExecutedToolConformance(
brunchToolCatalogue.filter(
({ name }) => name !== brunchTools.draftPetrinautExperiment,
),
),
).toThrow(/missing: draft_petrinaut_experiment/u);
});
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ test("an entity update keeps the turn the entity was created in", () => {
]);
});

test("ledger_compile renders the committed state as the agent-skin map", async () => {
test("ledger_compile renders the committed state as the Markdown map", async () => {
const history = settledHistory([
toBatch([
[
Expand Down Expand Up @@ -968,7 +968,6 @@ test("an obligation stays owed until a reflection discharges it", async () => {
expect(
renderLedgerMarkdown(
projectLedger(foldCommits(settledHistory([owing])).state),
"agent",
),
).toContain(
"## Checks still owed\n\n- A run at the current settings reproduces the 2 July overflow. \u2014 The 2 July overflow `o1`",
Expand Down Expand Up @@ -1008,7 +1007,6 @@ test("an obligation stays owed until a reflection discharges it", async () => {
expect(
renderLedgerMarkdown(
projectLedger(foldCommits(settledHistory([owing, discharging])).state),
"agent",
),
).not.toContain("Checks still owed");
});
Expand Down Expand Up @@ -1158,7 +1156,7 @@ describe.skipIf(!live).concurrent("kind enums with a live model", () => {
const ledger =
prior.length === 0
? "There are no existing entities or claims, so use only create routes."
: `The ledger already holds the records below; reference them by their IDs and use create routes for new records.\n\n${renderLedgerMarkdown(projectLedger(foldCommits(settledHistory(prior)).state), "agent")}\n\n`;
: `The ledger already holds the records below; reference them by their IDs and use create routes for new records.\n\n${renderLedgerMarkdown(projectLedger(foldCommits(settledHistory(prior)).state))}\n\n`;
const models = createModels();
models.setProvider(anthropicProvider());
models.setProvider(openaiProviderWithAddedModels());
Expand Down
4 changes: 2 additions & 2 deletions apps/brunch-agent/src/agents/petrinaut/tools/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const createLedgerCommitTool = (
},
});

/** `ledger_compile`: the committed Ledger rendered as the agent-skin map. */
/** `ledger_compile`: the committed Ledger rendered as the Markdown map. */
export const createLedgerCompileTool = (
readHistory: () => Promise<LedgerHistory>,
) =>
Expand All @@ -57,7 +57,7 @@ export const createLedgerCompileTool = (
output: {
revision,
rejected: [...rejected],
map: renderLedgerMarkdown(projectLedger(state), "agent"),
map: renderLedgerMarkdown(projectLedger(state)),
},
terminate: false,
};
Expand Down
Loading
Loading