Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
93fd9c0
Cut Mission 5 direct Voice transport
lunelson Sep 3, 2026
62940a8
Align future drafts with live Mission 5
lunelson Sep 3, 2026
181ef5e
Clarify Mission 5 direct transport proof
lunelson Sep 3, 2026
6393cbb
Recut Mission 5 around one Flue conversation route
lunelson Sep 3, 2026
360a0cd
Align future planning record with the recut Mission 5 route
lunelson Sep 3, 2026
81d69fc
Adopt the product-manager litmus in live Mission 5
lunelson Sep 3, 2026
b3ea38c
Point the spine litmus at the live Mission 5 surface
lunelson Sep 3, 2026
3ac0f6d
Admit Voice submission through the panel's Flue transport
lunelson Sep 3, 2026
1115b51
Expose the Flue browser chat transport
lunelson Sep 3, 2026
20079de
Wire the Petrinaut panel to the Flue route
lunelson Sep 3, 2026
61d02eb
Remove the legacy Petrinaut chat route
lunelson Sep 3, 2026
da989b6
Complete the Voice lifecycle over Flue
lunelson Sep 3, 2026
130099b
Document the unified Brunch conversation lifecycle
lunelson Sep 3, 2026
2b05ffe
Harden Mission 5 review fixes for hydration, TTS, and Clear
lunelson Sep 3, 2026
71b9042
Fix Mission 5 verification blockers
lunelson Sep 3, 2026
ceaa839
Record Mission 5 review verification
lunelson Sep 3, 2026
a37436e
Record Voice latency from Flue admission
lunelson Sep 3, 2026
495741e
Refresh Mission 5 evidence after admission timing
lunelson Sep 3, 2026
97e5175
Wait for Flue admission before accepting Voice tool results
lunelson Sep 3, 2026
35c4c90
Refresh Mission 5 evidence for Voice tool admission
lunelson Sep 3, 2026
bd530fd
Align Brunch integration references with the Flue route
lunelson Sep 3, 2026
7cc684c
Format conversation identity sources
lunelson Sep 4, 2026
34ad536
Correlate Stop with its Flue submission and align the two conversatio…
lunelson Sep 4, 2026
26f6e4b
Correlate Voice replies by every submission that wrote a message
lunelson Sep 4, 2026
ed048a9
Pin the Mission 5 witness to the current implementation
lunelson Sep 4, 2026
e43587c
Preserve Petrinaut Voice APIs in the Brunch preview
lunelson Sep 4, 2026
d1d5a08
Pin the Mission 5 witness to the Voice preview fix
lunelson Sep 4, 2026
df3cc18
Carry Voice resume defects into Mission 6
lunelson Sep 4, 2026
ad69010
Keep a turn busy across its client-tool follow-up and let Stop withho…
lunelson Sep 7, 2026
acc97ae
Reset assistant state with conversation identity
lunelson Sep 7, 2026
a69c2dd
Keep browser tool continuations causal
lunelson Sep 7, 2026
f82bfaf
Cut Mission 6 resumable fixture authority
lunelson Sep 3, 2026
c3e3811
Accept tagged prepared workpiece history
lunelson Sep 3, 2026
ad0fe68
Reconcile Mission 6 with browser Flue transport
lunelson Sep 3, 2026
5690c97
Record Mission 5 as the Mission 6 transport prerequisite
lunelson Sep 3, 2026
52e6216
Implement the Mission 6 resumable workpiece fixture over the Flue tra…
lunelson Sep 3, 2026
1fd0032
Correct Mission 6 bundle recovery and preparation
lunelson Sep 4, 2026
d3949a5
Normalize provider inputs for Petrinaut client tools
lunelson Sep 4, 2026
33dceb2
Retain Mission 6 outer browser witness
lunelson Sep 4, 2026
5383274
Simplify Mission 6 settlement coordination
lunelson Sep 4, 2026
b91c447
Normalize Mission 6 witness workpiece files
lunelson Sep 4, 2026
564fab0
Validate Mission 6 coherent bundle inputs
lunelson Sep 4, 2026
6de8bca
Replace the Mission 6 outer witness with honest authorship evidence
lunelson Sep 4, 2026
3216d83
M7+ resteering planning
lunelson Sep 4, 2026
2bc7f7d
M7+ resteering draft 2
lunelson Sep 4, 2026
985fab5
Incorporate the follow-up provenance review into the replanning record
lunelson Sep 4, 2026
6406432
Recut the Brunch mission spine around provenance by lineage with decl…
lunelson Sep 4, 2026
ae3d1c6
Amend Mission 6 deferred items for the recut planning record
lunelson Sep 4, 2026
8430132
Record Mission 6 cold-reader acceptance
lunelson Sep 4, 2026
21e6aea
Reposition the prepared fixture status banner
lunelson Sep 4, 2026
2e71732
Resume hydrated Petrinaut tool calls
lunelson Sep 4, 2026
a724c47
Tighten the Mission 7 draft after the readiness review
lunelson Sep 4, 2026
a63b353
Resume Petrinaut browser tool continuations
lunelson Sep 4, 2026
86aac55
Avoid deep Immer typing in prepared snapshot persistence
lunelson Sep 7, 2026
874327f
Close Mission 6 with deferred presentation checks
lunelson Sep 7, 2026
2de8148
Fix Mission 6 review findings for fixture routing, tool catalog, and …
lunelson Sep 7, 2026
7bfbaaf
Reconcile fixture routing and history gaps
lunelson Sep 7, 2026
2f88438
Refuse settlement while a coherent snapshot is missing
lunelson Sep 7, 2026
275d1f3
Wait for every browser tool before continuing
lunelson Sep 7, 2026
8c8514b
Cut Mission 6b Voice reconciliation authority
lunelson Sep 7, 2026
1e49313
Integrate KA's Voice hardening above resumable browser work
lunelson Sep 7, 2026
7927a28
Complete the source import test joins and provenance record
lunelson Sep 7, 2026
3fb4fa8
Reconcile browser tool ownership with Voice termination and canonical…
lunelson Sep 7, 2026
176d6e3
Record Mission 6b verification and unresolved acceptance boundaries
lunelson Sep 7, 2026
949abcd
Record authorization for the review-only Mission 7 restack
lunelson Sep 7, 2026
bd30cd0
Refresh Mission 6b pins after the verified main restack
lunelson Sep 7, 2026
4122ab2
Keep browser tool continuations causal
lunelson Sep 7, 2026
7292d26
Require explicit evidence before fixture mutation
lunelson Sep 7, 2026
c5da8af
Accept Mission 6b with explicit limitations
lunelson Sep 7, 2026
bd69729
Stabilize Voice tracker adapters across renders
lunelson Sep 7, 2026
dcd37ad
Remove the duplicate causal-result regression
lunelson Sep 7, 2026
a0ca1cb
Preserve batched continuations through effect replay
lunelson Sep 7, 2026
234b82f
Prepare split-ownership Voice mission proposal
kostandinang Sep 7, 2026
aabbe7b
Clarify Voice prerequisite and no-wake proof gates
kostandinang Sep 7, 2026
90493c6
Prepare Flue recording request with executable baseline evidence
kostandinang Sep 7, 2026
0b6c623
Link the published Flue recording feature request
kostandinang Sep 8, 2026
8e45c9e
Revert the published Flue recording evidence update
kostandinang Sep 8, 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
5 changes: 5 additions & 0 deletions .changeset/durable-brunch-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Let hosts make the assistant's Stop action durable before Petrinaut cancels its local response stream, while preserving local-only cancellation for hosts that do not provide the new stop request.
5 changes: 5 additions & 0 deletions .changeset/flue-voice-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Add half-duplex Voice handoff, exact response and marked-question replay, live transcripts, compact Voice setup and playback controls, and persistent copyable errors. Keep the conversation busy through browser-tool continuations, withhold pending work on Stop, surface automatic-tool failures to Voice, and display stopped entries and surviving client-tool Voice origins supplied by canonical history.
6 changes: 6 additions & 0 deletions .changeset/resumable-petrinaut-workpiece.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashintel/petrinaut": patch
"@hashintel/petrinaut-core": patch
---

Report an AI mutation that leaves the document unchanged as not applied, without claiming the requested state was already present, so hosts can distinguish an applied document change from an unchanged one, and reliably continue consecutive browser tool calls from live or rehydrated assistant messages.
5 changes: 5 additions & 0 deletions .changeset/voice-input-withdrawal-and-stop-correlation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Let hosts withdraw a retained voice input through an `AbortSignal`, keep a late durable Stop result from cancelling a newer turn, replace all conversation-owned assistant state when the conversation identity changes, hydrate host-owned history only once it carries every locally streamed reply, keep the composer status busy across the automatic follow-up to a client-tool step, and let a Stop pressed during that step withhold the follow-up.
42 changes: 15 additions & 27 deletions apps/brunch-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,36 @@ From the repository root, make `ANTHROPIC_API_KEY` available in the environment
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 Flue chat agent composed from the context-independent core prompt in `@hashintel/brunch-agent/flue`, the SDCPN/Petrinaut instructions, modelling runbook skill, and `readPetrinautDoc` client tool in `@hashintel/brunch-agent-plugin-sdcpn`, and app-owned deployment/transport material. The skill is activated via `activate_skill`, with supporting resources disclosed via `read_skill_resource`; the app's only model-facing diagnostic tool is `ping`. There is no generalized 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.
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 `/agents/chat/*` to Brunch without changing the request origin or Flue protocol. The typed panel and Voice mode talk to one Flue chat agent composed from the context-independent core prompt in `@hashintel/brunch-agent/flue`, the SDCPN/Petrinaut instructions, modelling runbook skill, and `readPetrinautDoc` client tool in `@hashintel/brunch-agent-plugin-sdcpn`, and app-owned deployment material. The skill is activated via `activate_skill`, with supporting resources disclosed via `read_skill_resource`; the app's only model-facing diagnostic tool is `ping`. There is no generalized 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.

A headless Mission 3 drive (simulated expert, same `ChatAgent` door):

```sh
yarn workspace @apps/brunch-agent runbook:headless
```

`ANTHROPIC_API_KEY` is required. `BRUNCH_CHAT_MODEL` selects the interviewer
(default `claude-sonnet-4-5` for this script only). Artifacts write under
`libs/@hashintel/brunch-agent/docs/evidence/evaluations/vestera-runbook-headless/`
unless `BRUNCH_RUNBOOK_OUTPUT_DIR` is set.
`ANTHROPIC_API_KEY` is required. `BRUNCH_CHAT_MODEL` selects the interviewer (default `claude-sonnet-4-5` for this script only). Artifacts write under `libs/@hashintel/brunch-agent/docs/evidence/evaluations/vestera-runbook-headless/` unless `BRUNCH_RUNBOOK_OUTPUT_DIR` is set.

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=`.
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 browser-transport 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 panel rehydrates from the SDK's canonical conversation observation and does not resubmit or replay settled turns.

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.
The mounted Flue URL `/agents/chat/:instanceId` requires the principal and logical conversation identity in `x-brunch-principal` and `x-brunch-conversation`. The path id is the hash of those values, not a bearer token or trusted authentication.

Print a human-readable transcript of one conversation from that same Flue history (server already
running):
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
## Panel and Voice conversation route

A second input modality joins the same chat door. It is not a voice route and does not own
provider audio or session state.
Voice is a second input modality over the panel's conversation. It is not a Voice route and does not own provider audio or durable conversation 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). |
| | |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| URL | `/agents/chat/:instanceId`, called through the public Flue browser client and the same-origin local proxy |
| Identity | `x-brunch-principal` plus `x-brunch-conversation`; the server verifies that their hash matches the mounted instance id |
| Initial turn | One `FlueClient.send()` carrying `{ kind: "user", body }` |
| Client-tool follow-up | One `FlueClient.send()` carrying the `client-tool-result` signal for completed client-tool parts, correlated by `toolCallId` |
| Response | `FlueClient.wait()` chunks projected into one finite AI SDK UI-message stream; observation/history provides canonical rehydration |

`OPTIONS /api/chat` is the CORS preflight for that same contract.
Typed and finalized spoken turns use this same route. The panel's explicit **Stop** requests a conversation-wide Flue abort before cancelling its local stream. Local Voice interruption stops playback only and leaves canonical history unchanged.
2 changes: 1 addition & 1 deletion 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, and Petrinaut /api/chat door.",
"description": "Remote Brunch server and local Petrinaut development loop.",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
Expand Down
55 changes: 25 additions & 30 deletions apps/brunch-agent/petrinaut-local.vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* Local FE-1436 panel launcher.
*
* Loads the real hash Petrinaut website config, removes only its incumbent
* `/api/chat` dev handler, and sends that same-origin route to brunch's
* committed application server. The real panel, wrappers, and editor stay untouched;
* hash's tracked checkout stays clean.
* Loads the real hash Petrinaut website config and proxies Brunch's mounted
* Flue route to the committed application server. The real panel, API
* handlers, wrappers, and editor stay untouched; hash's tracked checkout
* stays clean.
*/

import { join, resolve } from "node:path";
Expand All @@ -13,29 +13,28 @@ import {
defineConfig,
loadConfigFromFile,
mergeConfig,
type PluginOption,
type UserConfig,
} from "vite";

import {
defaultChatOrigin,
petrinautLocalServer,
} from "./src/http/local-origins.ts";

const withoutIncumbentChatHandler = (
plugins: readonly PluginOption[],
): PluginOption[] =>
plugins.filter((plugin) => {
if (
plugin === false ||
plugin === null ||
plugin === undefined ||
Array.isArray(plugin) ||
typeof plugin !== "object" ||
!("name" in plugin)
) {
return true;
}
return plugin.name !== "petrinaut-api-dev";
interface PetrinautPanelConfigOptions {
readonly chatOrigin: string;
readonly loadedConfig: UserConfig;
readonly root: string;
}

export const mergePetrinautPanelConfig = ({
chatOrigin,
loadedConfig,
root,
}: PetrinautPanelConfigOptions): UserConfig =>
mergeConfig(loadedConfig, {
root,
server: petrinautLocalServer(chatOrigin),
});

export default defineConfig(async (environment) => {
Expand All @@ -44,6 +43,7 @@ export default defineConfig(async (environment) => {
throw new Error("PETRINAUT_WEBSITE_ROOT is required.");
}
const root = resolve(websiteRoot);
process.env.VITE_BRUNCH_CHAT_ENDPOINT ??= "/agents/chat";
// Babel resolves the React compiler plugin from the launched project's cwd,
// not from the imported config file. Match a native hash launch before the
// plugin begins transforming the real panel source.
Expand All @@ -57,14 +57,9 @@ export default defineConfig(async (environment) => {
throw new Error(`Could not load Petrinaut's Vite config from ${root}.`);

const chatOrigin = process.env.BRUNCH_CHAT_ORIGIN ?? defaultChatOrigin;
return mergeConfig(
{
...loaded.config,
plugins: withoutIncumbentChatHandler(loaded.config.plugins ?? []),
},
{
root,
server: petrinautLocalServer(chatOrigin),
},
);
return mergePetrinautPanelConfig({
chatOrigin,
loadedConfig: loaded.config,
root,
});
});
23 changes: 2 additions & 21 deletions apps/brunch-agent/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
/**
* The app's route map β€” one plain Flue chat agent plus Petrinaut's /api/chat door.
*
* Both doors require principal + conversation id. `/api/chat` takes the principal
* header and body `id`; `/agents/chat/:id` takes the same principal plus
* `x-brunch-conversation` and admits the request only when those re-derive the
* path id. The Flue instance id is derived, not a bearer token.
*/
/** The app's route map β€” one ownership-guarded Flue conversation door. */

import { readFile } from "node:fs/promises";

Expand All @@ -18,28 +11,16 @@ import { ChatAgent } from "./agents/chat-agent/agent.ts";
import { healthHandler } from "./health.ts";
import { assetHandler } from "./http/assets.ts";
import { agentOwnershipGuard } from "./http/ownership.ts";
import { createPetrinautChatHandler } from "./http/petrinaut-chat.ts";
import {
CHAT_AGENT_ROUTE,
HEALTH_ROUTE,
PETRINAUT_CHAT_ROUTE,
} from "./http/routes.ts";
import { CHAT_AGENT_ROUTE, HEALTH_ROUTE } from "./http/routes.ts";

instrument(createOpenTelemetryInstrumentation({ content: false }));

const app = new Hono();
const appTransport: typeof fetch = async (input, init) =>
app.fetch(input instanceof Request ? input : new Request(input, init));
const petrinautChatHandler = createPetrinautChatHandler(appTransport);

const chatAgentMount = `/agents/${CHAT_AGENT_ROUTE}`;
app.use(`${chatAgentMount}/*`, agentOwnershipGuard(`${chatAgentMount}/`));
app.route(chatAgentMount, createAgentRouter(ChatAgent));

app.on(["GET", "POST", "OPTIONS"], PETRINAUT_CHAT_ROUTE, (c) =>
petrinautChatHandler(c.req.raw),
);

app.get(HEALTH_ROUTE, healthHandler);

const uiRoot = new URL(
Expand Down
10 changes: 7 additions & 3 deletions apps/brunch-agent/src/conversation/client-tools.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/** Flue-side client-tool signal contract: awaiting sentinel, result signal, tool names. */

import { READ_PETRINAUT_DOC_TOOL_NAME } from "@hashintel/brunch-agent-plugin-sdcpn/flue";
import {
petrinautFixtureToolNames,
READ_PETRINAUT_DOC_TOOL_NAME,
} from "@hashintel/brunch-agent-plugin-sdcpn/flue";
import { CLIENT_TOOL_RESULT_SIGNAL } from "@hashintel/brunch-agent-transport-aisdk";
import { AWAITING_CLIENT } from "@hashintel/brunch-agent/client-tools";

export { AWAITING_CLIENT };

export const CLIENT_TOOL_RESULT_SIGNAL = "client-tool-result";
export { CLIENT_TOOL_RESULT_SIGNAL };

export const clientToolNames: ReadonlySet<string> = new Set([
READ_PETRINAUT_DOC_TOOL_NAME,
...petrinautFixtureToolNames,
]);

const isRecord = (value: unknown): value is Record<string, unknown> =>
Expand Down
14 changes: 0 additions & 14 deletions apps/brunch-agent/src/conversation/identity-web.ts

This file was deleted.

35 changes: 10 additions & 25 deletions apps/brunch-agent/src/conversation/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,32 @@
import { createHash, timingSafeEqual } from "node:crypto";

import {
BRUNCH_CONVERSATION_HEADER,
BRUNCH_PRINCIPAL_HEADER,
identityPayload,
} from "./payload.ts";

import type { ConversationIdentity } from "@hashintel/brunch-agent-transport-aisdk";
type ConversationIdentity,
} from "@hashintel/brunch-agent-transport-aisdk";

export {
agentOwnershipHeaders,
BRUNCH_CONVERSATION_HEADER,
BRUNCH_PRINCIPAL_HEADER,
LOCAL_UI_PRINCIPAL,
} from "./payload.ts";
} from "@hashintel/brunch-agent-transport-aisdk";
export { LOCAL_UI_PRINCIPAL } from "./payload.ts";
export type { ConversationIdentity };

export const flueConversationId = (
principalKey: string,
conversationId: string,
): string =>
createHash("sha256")
.update(identityPayload(principalKey, conversationId))
.digest("hex");
export const flueConversationId = (identity: ConversationIdentity): string =>
createHash("sha256").update(identityPayload(identity)).digest("hex");

export const flueConversationIdFrom = (
identity: ConversationIdentity,
): string => flueConversationId(identity.principalKey, identity.conversationId);
): string => flueConversationId(identity);

export const ownsFlueInstance = (
principalKey: string,
conversationId: string,
identity: ConversationIdentity,
instanceId: string,
): boolean => {
const expected = flueConversationId(principalKey, conversationId);
const expected = flueConversationId(identity);
const expectedBytes = Buffer.from(expected);
const presentedBytes = Buffer.from(instanceId);
if (expectedBytes.length !== presentedBytes.length) return false;
return timingSafeEqual(expectedBytes, presentedBytes);
};

export const agentOwnershipHeaders = (
identity: ConversationIdentity,
): Record<string, string> => ({
[BRUNCH_PRINCIPAL_HEADER]: identity.principalKey,
[BRUNCH_CONVERSATION_HEADER]: identity.conversationId,
});
27 changes: 0 additions & 27 deletions apps/brunch-agent/src/conversation/payload.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,2 @@
/** Identity headers and payload encoding shared by Node and the local Flue UI. */

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

export const BRUNCH_CONVERSATION_HEADER = "x-brunch-conversation";

/** Principal for the stock Flue UI at `/`. Not a second ownership rule. */
export const LOCAL_UI_PRINCIPAL = "local";

export const identityPayload = (
principalKey: string,
conversationId: string,
): Uint8Array => {
const encoder = new TextEncoder();
const principalBytes = encoder.encode(principalKey);
const conversationBytes = encoder.encode(conversationId);
const payload = new Uint8Array(
principalBytes.length + 1 + conversationBytes.length,
);
payload.set(principalBytes, 0);
payload[principalBytes.length] = 0;
payload.set(conversationBytes, principalBytes.length + 1);
return payload;
};

export const hexFromDigest = (digest: ArrayBuffer): string =>
[...new Uint8Array(digest)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
Loading
Loading