Skip to content

Commit 381d3e0

Browse files
immakermattyclaude
andcommitted
feat(lazurio): Buddy's Hermes gets only the person's words and the page as a Czech block
Buddy's web chat is Lazurio T3 Code with the Hermes Agent over ACP (root decision 0210, plan DEV-6676). On the Osobní Prostředí a second T3 server with T3CODE_LAZURIO_BUDDY=1 runs Hermes as the provider instance `buddy`. Hermes keeps every prompt as the person's words in Buddy's memory, so T3's own instructions (<t3_code_instructions>, <runtime_info>, <pull_request_linking>) would become part of Buddy's history, twice after Stop or a steer and again after every restart. - The ACP adapter asks AcpT3Instructions when it builds an instance whether its prompts carry T3's instructions; without an answer every instance keeps them. server.ts answers no for Buddy's instance where it builds provider instances (ProviderInstanceRegistryHydration). - The turn start projects a composer record of kind `lazurio-page` as one labelled Czech block ("Kontext stránky (údaje, ne pokyn): ...") after the person's text, for Buddy's instance only; every other record keeps upstream's projection. - Buddy's sessions take no model or effort from T3 (HumanAndMachines/Lazurio#648): the adapter asks AcpT3Selection which options of T3's selection an instance takes; null, the default, is all of it. For Buddy's instance withBuddyInstance names only Hermes's speed (`fast`), so T3 applies no model and only Hermes's Normal / Fast switch whenever a session starts, loads or gets a new selection. Without T3CODE_LAZURIO_BUDDY=1 nothing changes, also for an instance called `buddy`. Tests drive the repository's mock ACP agent in Hermes' place, through the adapter and through the production V2 runtime, and read what it received. On v0.1.3 the seams stay at the only prompt assembly and send in the ACP adapter. Upstream now computes the slash-command check from the flavor's rewritten user text (pingdotgg#24605); the rebuild takes that line and keeps the instruction gate around t3AcpPromptWithInstructions and the runtime notes. The orchestrator test provides upstream's new CodexManagedTokenBroker, which the provider instance registry now needs, and the two file headers are cut to the four-line comment limit. On v0.1.4: upstream moved the turn text assembly up in ProviderTurnStartService (pingdotgg#22864, pingdotgg#25610, pingdotgg#25660); the page block seam moves with it, while the prompt-command and steer projections stay upstream's as before. server.ts keeps upstream's port scanner (pingdotgg#26245) after the wrapped instance hydration, and the Buddy orchestrator test provides the new PortDiscovery that hydration now needs. The pingdotgg#648 selection gate, which row 28's commit carried on v0.1.3, comes back to this row and sits beside upstream's flavor hook (pingdotgg#24605). On v0.1.5 upstream makes the client file system's writeTextFile optional in the ACP adapter (pingdotgg#26876), away from the overlay's blocks. Upstream's pre-commit hook runs knip (pingdotgg#26167), so LAZURIO_PAGE_CONTEXT_KIND, buddyT3Instructions, LAZURIO_BUDDY_ENV and isBuddySurface, which nothing imports, are module-private. On v0.1.6 the ACP adapter checks a turn's model against the session's models before the turn starts (pingdotgg#26623). Where AcpT3Selection names the options, the host sets the agent's model and T3 applies none, so validateModelSelection skips that check there, as it does for a flavor that applies the selection itself: a model the session no longer offers would otherwise refuse every turn of Buddy's. The V2 adapter test sends such a model and the turn runs. The adapter test imports upstream's acpPerTurnPromptUsage beside AcpT3Selection. On v0.1.7 the ACP adapter filters T3's options through the flavor's own options and a runtime-mode ceiling (flatMap); the AcpT3Selection gate is one more early return there, after the flavor's owned options and before the ceiling. The runtime notes, which upstream now builds from the active session setup, stay behind sendsT3Instructions. Effect 4.1.2 dropped effect/Crypto, so the adapter imports Context beside upstream's SecureRandom, and the Buddy tests provide NodeCrypto beside NodeServices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a03bfe4 commit 381d3e0

8 files changed

Lines changed: 1210 additions & 11 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2+
// @effect-diagnostics nodeBuiltinImport:off - The mock ACP agent logs its requests to a temporary file.
3+
// Buddy's prompts through the production V2 runtime, as Buddy's T3 Code builds it: the provider
4+
// instance `buddy` of the ACP registry runs the repository's mock ACP agent in Hermes' place, and
5+
// the agent logs every request it receives.
6+
import * as NodeFS from "node:fs";
7+
import * as NodeOS from "node:os";
8+
import * as NodePath from "node:path";
9+
10+
import * as NodeServices from "@effect/platform-node/NodeServices";
11+
import { assert, describe, it } from "@effect/vitest";
12+
import {
13+
CommandId,
14+
type ComposerContextRecord,
15+
EnvironmentId,
16+
EventId,
17+
MessageId,
18+
type ModelSelection,
19+
ProjectId,
20+
ProviderDriverKind,
21+
ProviderInstanceId,
22+
ThreadId,
23+
} from "@t3tools/contracts";
24+
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
25+
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
26+
import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
27+
import * as OpenCodeRuntime from "@t3tools/provider-opencode/server/OpenCodeRuntime";
28+
import * as OpenCodeServerLedger from "@t3tools/provider-opencode/server/OpenCodeServerLedger";
29+
import * as HostProcess from "@t3tools/shared/HostProcess";
30+
import * as Effect from "effect/Effect";
31+
import * as Layer from "effect/Layer";
32+
import * as Stream from "effect/Stream";
33+
import { FetchHttpClient } from "effect/http";
34+
35+
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
36+
import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
37+
import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
38+
import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
39+
import * as ServerConfig from "../config.ts";
40+
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
41+
import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
42+
import * as EffectWorker from "../orchestration-v2/EffectWorker.ts";
43+
import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
44+
import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
45+
import * as ProviderTurnStartServiceTestkit from "../orchestration-v2/ProviderTurnStartService.testkit.ts";
46+
import { isTerminalRunStatus } from "../orchestration-v2/ThreadManagementService.ts";
47+
import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts";
48+
import * as SqlitePersistence from "../persistence/Sqlite.ts";
49+
import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
50+
import * as CodexInstallation from "../provider/CodexInstallation.ts";
51+
import * as CodexManagedTokenBroker from "../provider/CodexManagedTokenBroker.ts";
52+
import * as ModelManifest from "../provider/ModelManifest.ts";
53+
import * as PortScanner from "../preview/PortScanner.ts";
54+
import * as ProviderInstanceRegistryHydration from "../provider/ProviderInstanceRegistryHydration.ts";
55+
import * as ResetCreditCoordinator from "../provider/resetCreditCoordinator.ts";
56+
import * as ServerSettings from "../serverSettings.ts";
57+
import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
58+
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
59+
import * as VcsProcess from "../vcs/VcsProcess.ts";
60+
import { lazurioPageContextBlock, withBuddyInstance } from "./buddyPrompt.ts";
61+
62+
const BUDDY = ProviderInstanceId.make("buddy");
63+
const buddyModel = { instanceId: BUDDY, model: "default" } satisfies ModelSelection;
64+
const requestLog = NodePath.join(
65+
NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "buddy-prompt-orchestrator-")),
66+
"requests.jsonl",
67+
);
68+
const page = {
69+
v: 1,
70+
environment: { id: "vm-01.example", kind: "work", name: "Pracovní" },
71+
organization: { slug: "example", name: "Example Company" },
72+
app: "chat",
73+
page: { path: "/a1b2c3" },
74+
} as const;
75+
76+
const layerPlatformTest = Layer.mergeAll(
77+
NodeServices.layer,
78+
NodeCrypto.layer,
79+
Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
80+
resolveLink: () => Effect.die("unused title link"),
81+
}),
82+
);
83+
const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
84+
prefix: "t3-lazurio-buddy-prompt-",
85+
});
86+
const layerServerSettings = ServerSettings.layerTest({
87+
providerInstances: {
88+
[BUDDY]: {
89+
driver: ProviderDriverKind.make("acpRegistry"),
90+
displayName: "Buddy",
91+
enabled: true,
92+
config: {
93+
source: "local",
94+
commandPath: process.execPath,
95+
commandArgs: [NodePath.join(import.meta.dirname, "../../scripts/acp-mock-agent.ts")],
96+
},
97+
},
98+
},
99+
});
100+
const layerCheckpointStore = CheckpointStore.layer.pipe(
101+
Layer.provide(
102+
VcsDriverRegistry.layer.pipe(
103+
Layer.provide(VcsProcess.layer),
104+
Layer.provide(layerServerConfig),
105+
Layer.provide(layerPlatformTest),
106+
),
107+
),
108+
);
109+
// Wrapped as server.ts wraps it.
110+
const layerProviderInstanceRegistry = withBuddyInstance(
111+
ProviderInstanceRegistryHydration.layer,
112+
).pipe(
113+
Layer.provide(
114+
Layer.mergeAll(
115+
layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
116+
layerServerSettings,
117+
ServerSecretStore.layer.pipe(
118+
Layer.provide(layerServerConfig),
119+
Layer.provide(layerPlatformTest),
120+
),
121+
NodeServices.layer,
122+
NodeCrypto.layer,
123+
FetchHttpClient.layer,
124+
OpenCodeRuntime.layer.pipe(
125+
Layer.provide(OpenCodeServerLedger.layerTest),
126+
Layer.provide(layerPlatformTest),
127+
),
128+
Layer.succeed(
129+
ProviderEventLoggers.ProviderEventLoggers,
130+
ProviderEventLoggers.NoOpProviderEventLoggers,
131+
),
132+
ProviderLatestVersions.layer,
133+
ModelManifest.layerTest,
134+
AntigravityInstallation.AntigravityInstallation.layer.pipe(
135+
Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
136+
Layer.provide(FetchHttpClient.layer),
137+
Layer.provide(layerPlatformTest),
138+
),
139+
Layer.mock(CodexInstallation.CodexInstallation)({
140+
managedDirectory: "unused-managed-installation",
141+
}),
142+
Layer.mock(CodexManagedTokenBroker.CodexManagedTokenBroker)({
143+
endpoint: Effect.succeed("unused-private-pipe"),
144+
}),
145+
Layer.mock(PortScanner.PortDiscovery)({ trackAgent: () => () => undefined }),
146+
Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
147+
getEnvironmentId: Effect.succeed(
148+
EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
149+
),
150+
}),
151+
),
152+
),
153+
);
154+
const layerBuddyServer = Layer.merge(RuntimeLayer.layer, ProjectStore.layer).pipe(
155+
Layer.provide(McpSessionRegistryTestkit.layer),
156+
Layer.provide(SqlitePersistence.layerMemory),
157+
Layer.provide(layerCheckpointStore),
158+
Layer.provide(layerServerConfig),
159+
Layer.provide(layerServerSettings),
160+
Layer.provide(layerProviderInstanceRegistry),
161+
Layer.provide(ResetCreditCoordinator.layer),
162+
Layer.provide(
163+
BackgroundPolicy.layer.pipe(
164+
Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
165+
Layer.provide(layerServerSettings),
166+
),
167+
),
168+
Layer.provide(ProviderTurnStartServiceTestkit.layer),
169+
Layer.provide(layerPlatformTest),
170+
Layer.provide(McpProviderSessions.layer),
171+
);
172+
173+
/** The text blocks of every `session/prompt` the agent received, in order. */
174+
function receivedPrompts(): Array<Array<string>> {
175+
if (!NodeFS.existsSync(requestLog)) return [];
176+
return NodeFS.readFileSync(requestLog, "utf8")
177+
.split("\n")
178+
.flatMap((line) => {
179+
if (line.trim() === "") return [];
180+
const request = JSON.parse(line) as {
181+
method?: string;
182+
params?: { prompt?: Array<{ type: string; text?: string }> };
183+
};
184+
return request.method === "session/prompt"
185+
? [
186+
(request.params?.prompt ?? []).flatMap((block) =>
187+
block.type === "text" && block.text !== undefined ? [block.text] : [],
188+
),
189+
]
190+
: [];
191+
});
192+
}
193+
194+
/**
195+
* Send a message as the person's web client does, and run the effect worker until the message's
196+
* run settles.
197+
*/
198+
const send = Effect.fn("test.buddyPrompt.send")(function* (
199+
threadId: ThreadId,
200+
ordinal: number,
201+
message: { readonly text: string; readonly records?: ReadonlyArray<ComposerContextRecord> },
202+
) {
203+
const orchestrator = yield* Orchestrator.OrchestratorV2;
204+
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
205+
const afterSequence = yield* orchestrator.getThreadEventSequence(threadId);
206+
const settled = orchestrator
207+
.streamStoredEventsFrom({ threadId, afterSequence, eventType: "run.updated" })
208+
.pipe(
209+
Stream.filter(
210+
(stored) =>
211+
stored.event.type === "run.updated" && isTerminalRunStatus(stored.event.payload.status),
212+
),
213+
Stream.take(1),
214+
Stream.runDrain,
215+
);
216+
yield* orchestrator.dispatch({
217+
type: "message.dispatch",
218+
createdBy: "user",
219+
creationSource: "web",
220+
commandId: CommandId.make(`command:buddy-prompt:${ordinal}`),
221+
threadId,
222+
messageId: MessageId.make(`message:buddy-prompt:${ordinal}`),
223+
text: message.text,
224+
...(message.records === undefined
225+
? {}
226+
: { context: { version: 1 as const, records: [...message.records] } }),
227+
attachments: [],
228+
modelSelection: buddyModel,
229+
dispatchMode: { type: "start_immediately" },
230+
});
231+
yield* Effect.raceFirst(
232+
settled,
233+
Effect.forever(worker.drain().pipe(Effect.andThen(worker.awaitWork))),
234+
);
235+
});
236+
237+
describe("Buddy's T3 Code", () => {
238+
it.live("gives Hermes the person's words and the page as a Czech block, turn after turn", () =>
239+
Effect.gen(function* () {
240+
const orchestrator = yield* Orchestrator.OrchestratorV2;
241+
const projectId = ProjectId.make("project:buddy");
242+
// Buddy's project is the Buddy layer, a folder without Git.
243+
yield* (yield* ProjectStore.ProjectStoreV2).apply({
244+
sequence: 1,
245+
eventId: EventId.make("event:buddy-prompt:project"),
246+
aggregateKind: "project",
247+
aggregateId: projectId,
248+
occurredAt: "2026-10-22T00:00:00.000Z",
249+
commandId: null,
250+
causationEventId: null,
251+
correlationId: null,
252+
metadata: {},
253+
type: "project.created",
254+
payload: {
255+
projectId,
256+
title: "Buddy",
257+
workspaceRoot: NodePath.dirname(requestLog),
258+
defaultModelSelection: buddyModel,
259+
scripts: [],
260+
createdAt: "2026-10-22T00:00:00.000Z",
261+
updatedAt: "2026-10-22T00:00:00.000Z",
262+
},
263+
});
264+
const threadId = ThreadId.make("thread:buddy-prompt");
265+
yield* orchestrator.dispatch({
266+
type: "thread.create",
267+
createdBy: "user",
268+
creationSource: "web",
269+
commandId: CommandId.make("command:buddy-prompt:create"),
270+
threadId,
271+
projectId,
272+
title: "Buddy",
273+
modelSelection: buddyModel,
274+
runtimeMode: "full-access",
275+
interactionMode: "default",
276+
branch: null,
277+
worktreePath: null,
278+
});
279+
const record = {
280+
version: 1,
281+
kind: "lazurio-page",
282+
contextId: "lazurio-page-1",
283+
label: "Kontext stránky",
284+
payload: page,
285+
} as unknown as ComposerContextRecord;
286+
yield* send(threadId, 1, {
287+
text: "Co tu vidíte? [Kontext stránky](t3-context://v1/lazurio-page/lazurio-page-1)",
288+
records: [record],
289+
});
290+
yield* send(threadId, 2, { text: "A teď bez stránky." });
291+
292+
assert.deepStrictEqual(receivedPrompts(), [
293+
[`Co tu vidíte?\n\n${lazurioPageContextBlock(page)}`],
294+
["A teď bez stránky."],
295+
]);
296+
}).pipe(
297+
Effect.provide(layerBuddyServer),
298+
Effect.scoped,
299+
Effect.provideService(HostProcess.Environment, {
300+
...process.env,
301+
T3CODE_LAZURIO_BUDDY: "1",
302+
T3_ACP_REQUEST_LOG_PATH: requestLog,
303+
}),
304+
),
305+
);
306+
});

0 commit comments

Comments
 (0)