Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/await-ai-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Describe a clean net-code diagnostic check without claiming the whole model compiles.
5 changes: 5 additions & 0 deletions .changeset/snapshot-language-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

Describe snapshot validation and its TypeScript and HIR diagnostics in the AI tool.
3 changes: 1 addition & 2 deletions apps/brunch-agent/test/compiler-feedback.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ import { openBrowserFixture } from "./browser-fixture.ts";
import { browserResultFrom } from "./browser-result.ts";
import { nativeSchemaProvider } from "./native-schema-provider.ts";

const cleanCompilation =
"No errors detected in your model – everything compiles!";
const cleanCompilation = "No errors or warnings found in net function code.";
const output = mkdtempSync(join(tmpdir(), "m7c-compiler-feedback-"));
const save = (name: string, value: unknown) =>
writeFileSync(join(output, `${name}.json`), JSON.stringify(value, null, 2));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test.skipIf(!enabled)(
expect(summary.mode).toBe("batched-construction");
expect(summary.dirtyCompilation).toContain("definitelyNotDefined");
expect(summary.cleanCompilation).toBe(
"No errors detected in your model – everything compiles!",
"No errors or warnings found in net function code.",
);
expect(summary.repairHash).toMatch(/^[a-f0-9]{64}$/u);
expect(summary.layoutHash).toMatch(/^[a-f0-9]{64}$/u);
Expand Down
4 changes: 2 additions & 2 deletions apps/brunch-agent/test/root-creation.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ try {
save("compilation", result);
assert.equal(
result.output,
"No errors detected in your model – everything compiles!",
"No errors or warnings found in net function code.",
);
return text("Native creation and canonical check completed.");
}),
Expand Down Expand Up @@ -504,7 +504,7 @@ try {
requests: contexts.length,
applied: records.length,
schemaClasses: observedNodeMutationNames,
compilation: "No errors detected in your model – everything compiles!",
compilation: "No errors or warnings found in net function code.",
scope:
"Same-session synthetic creation/correction only; reopen assertion follows.",
});
Expand Down
2 changes: 1 addition & 1 deletion apps/brunch-agent/test/typed-state.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ try {
compilations.push(result);
assert.equal(
result.output,
"No errors detected in your model – everything compiles!",
"No errors or warnings found in net function code.",
"Final corrected net must report clean canonical diagnostics; no scenario execution follows",
);
return text(
Expand Down
2 changes: 1 addition & 1 deletion libs/@hashintel/petrinaut-core/src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const getLatestNetDefinitionToolInputSchema = z
const getNetCompilationErrorsToolInputSchema = z
.strictObject({})
.describe(
"Get the current TypeScript diagnostics for the Petrinaut net code. Use this after the net to check whether the model compiles.",
"Validate the current Petrinaut net snapshot and return its TypeScript and HIR diagnostics.",
);

export const setNetTitleToolInputSchema = z
Expand Down
49 changes: 49 additions & 0 deletions libs/@hashintel/petrinaut-core/src/lsp/language-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,55 @@ describe("createLanguageClient diagnostics", () => {
]);
});

it("correlates snapshot responses without waiting for diagnostic changes", async () => {
const { transport, publish, respond, sent } = createFakeTransport();
const client = createLanguageClient({ transport });
const definition = {
places: [],
transitions: [],
parameters: [],
types: [],
differentialEquations: [],
};
const first = client.requestDiagnostics(definition);
const second = client.requestDiagnostics(definition);
publish([
{
uri: "inmemory://session",
diagnostics: [diagnostic("unrelated session")],
},
]);
respond(1, []);
respond(0, [
{ uri: "inmemory://net", diagnostics: [diagnostic("net error")] },
]);

expect((await first).byUri.get("inmemory://net")?.[0]?.message).toBe(
"net error",
);
expect((await second).errorCount).toBe(0);
expect(client.diagnostics.get().byUri.has("inmemory://session")).toBe(true);
expect(sent).toContainEqual({
jsonrpc: "2.0",
id: 0,
method: "sdcpn/diagnostics",
params: { sdcpn: definition, extensions: undefined },
});
});

it("rejects a snapshot request when the worker is disposed", async () => {
const { transport } = createFakeTransport();
const client = createLanguageClient({ transport });
const response = client.requestDiagnostics({
places: [],
transitions: [],
parameters: [],
types: [],
differentialEquations: [],
});
client.dispose();
await expect(response).rejects.toThrow();
});
it("notifies subscribers only when a publish changed a diagnostic", () => {
const { transport, publish } = createFakeTransport();
const client = createLanguageClient({ transport });
Expand Down
6 changes: 5 additions & 1 deletion libs/@hashintel/petrinaut/docs/ai-assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ The composer stays open in all of these cases, so you can still ask questions, r

## Diagnostics integration

When the assistant edits code surfaces (lambdas, kernels, dynamics, visualizers, metric/scenario code), it sees the resulting TypeScript diagnostics on the next turn and can iteratively fix them. You don't have to relay errors manually -- the post-edit re-check happens automatically. The same diagnostics also appear in the bottom **Diagnostics** tab as usual; the assistant just sees them in addition.
The assistant can request a fresh TypeScript check of the current net and
use the returned errors to revise its code. An unchanged set of errors still
counts as a completed check. If checking fails, the assistant receives an
error. The bottom **Diagnostics** tab continues to show diagnostics for the
code you are editing.

## Host configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,12 @@ vi.mock("@hashintel/ds-components", async (importOriginal) => {
*/
function makeLanguageClient(): LanguageClientContextValue {
return {
requestDiagnostics: vi.fn(() =>
Promise.resolve({ byUri: new Map(), total: 0, errorCount: 0 }),
),
diagnosticsByUri: new Map(),
totalDiagnosticsCount: 0,
errorDiagnosticsCount: 0,
requestDiagnostics: vi.fn(),
notifyDocumentChanged: vi.fn(),
requestCompletion: vi.fn(() =>
Promise.resolve({ isIncomplete: false, items: [] }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ vi.mock("@hashintel/ds-components", () => {

function makeLanguageClientValue(): LanguageClientContextValue {
return {
requestDiagnostics: vi.fn(() =>
Promise.resolve({ byUri: new Map(), total: 0, errorCount: 0 }),
),
diagnosticsByUri: new Map(),
totalDiagnosticsCount: 0,
errorDiagnosticsCount: 0,
requestDiagnostics: vi.fn(),
notifyDocumentChanged: vi.fn(),
requestCompletion: vi.fn(() =>
Promise.resolve({ isIncomplete: false, items: [] }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5279,7 +5279,7 @@ describe("AiAssistantPanel host interactive tools", () => {
expect(diagnosticsOutputs).toHaveLength(2);
expect(
diagnosticsOutputs.every((output) =>
output.includes("everything compiles"),
output.includes("No errors or warnings found in net function code."),
),
).toBe(true);
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ const diagnostic = (
});

describe("formatDiagnosticsForAi", () => {
test("reports an empty diagnostics state", () => {
test("reports that no errors or warnings were found in net function code", () => {
expect(
formatDiagnosticsForAi({
definition,
diagnosticsByUri: new Map(),
}),
).toBe("No errors detected in your model – everything compiles!");
).toBe("No errors or warnings found in net function code.");
});

test("formats transition and differential-equation diagnostics", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const formatDiagnosticsForAi = ({
);

if (diagnostics.length === 0) {
return "No errors detected in your model – everything compiles!";
return "No errors or warnings found in net function code.";
}

const shownDiagnostics = diagnostics.slice(0, maxDiagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("readCurrentDiagnostics", () => {
try {
await expect(
readCurrentDiagnostics(instance, request),
).resolves.toContain("everything compiles");
).resolves.toContain("No errors or warnings found in net function code.");
instance.mutations.addParameter({
id: "rate",
name: "Rate",
Expand All @@ -40,7 +40,7 @@ describe("readCurrentDiagnostics", () => {
});
await expect(
readCurrentDiagnostics(instance, request),
).resolves.toContain("everything compiles");
).resolves.toContain("No errors or warnings found in net function code.");
expect(
request.mock.calls.map(([definition]) => definition.parameters.length),
).toEqual([0, 1]);
Expand Down Expand Up @@ -69,7 +69,9 @@ describe("readCurrentDiagnostics", () => {
});
result.resolve({ byUri: new Map(), total: 0, errorCount: 0 });
await expect(read).resolves.toMatch(/changed.*check again/iu);
await expect(read).resolves.not.toMatch(/everything compiles/iu);
await expect(read).resolves.not.toContain(
"No errors or warnings found in net function code.",
);
} finally {
instance.dispose();
}
Expand Down
4 changes: 2 additions & 2 deletions libs/@hashintel/petrinaut/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const externalDependencies = [
"@hashintel/ds-components",
"@hashintel/ds-helpers",
/^@hashintel\/petrinaut-core(\/.*)?$/,
"react",
"react-dom",
/^react(\/.*)?$/,
/^react-dom(\/.*)?$/,
"@xyflow/react",
"@babel/standalone",
// Pure-CJS dep pulled in transitively by @tanstack/react-form →
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Snapshot diagnostics
description: Check a captured net through a correlated language-worker request.
attachTo: core.lsp
sidebar_order: 20
---

`LanguageClient.requestDiagnostics(sdcpn, extensions)` returns TypeScript and HIR
diagnostics for the supplied net snapshot. Callers receive a response even
when the diagnostics match the previous check.

![Snapshot diagnostics flow](@diagrams/snapshot-diagnostics.svg)

The [language worker](layer:core.lsp.worker) checks the snapshot separately
from temporary editor sessions. Its response carries the request ID, so an
editor diagnostic update cannot complete another caller's check. Worker
errors and language-client disposal reject pending requests.

The [React language provider](layer:react.lsp) exposes this method to the AI's
`getNetCompilationErrors` tool. The tool captures the current net and formats
the returned diagnostics. The editor continues to receive its own diagnostic
updates. See [compiling user code](doc:simulation/user-code) for the distinction
between TypeScript diagnostics and simulation compilation.

:::danger[Important: worker timeout]
Requests have no timeout. An unresponsive worker can leave a check pending
until the language client is disposed.
Tracked in [FE-1665](https://linear.app/hash/issue/FE-1665).
:::

:::danger[Important: instance API]
AI diagnostics still use the React language provider. Exposing this capability
through `PetrinautInstance` requires a headless diagnostics contract.
Tracked separately in [FE-754](https://linear.app/hash/issue/FE-754).
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
direction: down
vars: {d2-config: {theme-id: 0}}
client: "Language client"
worker: "Language worker"
snapshot: "Check supplied snapshot"
reply: "Resolve matching request"
client -> worker: "snapshot + ID"
worker -> snapshot
snapshot -> reply: "diagnostics + ID"
reply -> client: "result / error"
Loading