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
18 changes: 17 additions & 1 deletion apps/demo/lib/hands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('each drive builds the harness for its session’s hands', () => {
it('builds one harness per hands, once, and sends each drive to its own', async () => {
const built: string[] = [];
const drove: string[] = [];
const harness = byHands('pi', undefined, async (hands) => {
const harness = byHands('pi', {}, async (hands) => {
built.push(hands);
return { name: 'pi', drive: async (ctx) => void drove.push(`${hands}:${ctx.runId}`) } satisfies Harness;
});
Expand All @@ -131,4 +131,20 @@ describe('each drive builds the harness for its session’s hands', () => {
expect(built).toEqual(['none', 'machine', 'virtual']);
expect(drove).toEqual(['none:a', 'machine:b', 'virtual:c', 'none:a']);
});

it('declares how its model calls are recorded before the first build, and warns when the harness disagrees', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const ctx = { runId: 'a', config: { hands: 'none' } } as Parameters<Harness['drive']>[0];
const self = byHands('pi', { modelCalls: 'self' }, async () => ({ name: 'pi', modelCalls: 'self', drive: async () => {} }));
expect(self.modelCalls).toBe('self');
await self.drive(ctx);
expect(warn).not.toHaveBeenCalled();
const effects = byHands('pi', { modelCalls: 'self' }, async () => ({ name: 'pi', drive: async () => {} }));
await effects.drive(ctx);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("declares modelCalls 'self', the harness 'effects'"));
} finally {
warn.mockRestore();
}
});
});
32 changes: 27 additions & 5 deletions apps/demo/lib/hands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sandboxName } from 'durable-harness/sandbox';
import type { Harness, LaneOptions } from 'harness-world';
import type { Harness } from 'harness-world';
import type { Brain, Hands, HarnessOption } from 'harness-world-demo-ui';
import { BUILTIN_HARNESSES, handsReason, isHands, legacyHands, pickHands, sandboxOffReason as sandboxOffCopy } from 'harness-world-demo-ui/axes';

Expand All @@ -16,21 +16,43 @@ export function handsOfConfig(config: unknown, harness: string): Hands {
return isHands(hands) ? hands : legacyHands(harness);
}

/**
* What a harness built on its first drive declares before it exists: its
* `lanes`, and how Agent Runs learns its model calls. durable-harness's
* tracing reads `modelCalls` when a drive starts, before the build: a
* wrapper that leaves it out gets every model call recorded twice (an empty
* `chat` span from the `model` effect next to the harness's own).
*/
export type UpFront = Pick<Harness, 'lanes' | 'modelCalls'>;

/** The fields of `upFront` that are set, for the wrapper. */
export function upFrontFields(upFront: UpFront): UpFront {
return { ...(upFront.lanes ? { lanes: upFront.lanes } : {}), ...(upFront.modelCalls ? { modelCalls: upFront.modelCalls } : {}) };
}

/** `built`, with a warning when its `modelCalls` is not what its wrapper declared up front. */
export function checkUpFront(name: string, upFront: UpFront, built: Harness): Harness {
const declared = upFront.modelCalls ?? 'effects';
const actual = built.modelCalls ?? 'effects';
if (actual !== declared) console.warn(`[harness] ${name}: its wrapper declares modelCalls '${declared}', the harness '${actual}': Agent Runs records its model calls wrongly`);
return built;
}

/**
* One kernel harness that is really one per hands: each is built on the
* first drive of a session with those hands (in this process), and each
* drive goes to its session's. `name` and `lanes` are known up front.
* drive goes to its session's. `name` and `upFront` are known up front.
*/
export function byHands(name: string, lanes: LaneOptions | undefined, build: (hands: Hands) => Promise<Harness>): Harness {
export function byHands(name: string, upFront: UpFront, build: (hands: Hands) => Promise<Harness>): Harness {
const built = new Map<Hands, Promise<Harness>>();
return {
name,
...(lanes ? { lanes } : {}),
...upFrontFields(upFront),
async drive(ctx) {
const hands = handsOfConfig(ctx.config, name);
let harness = built.get(hands);
if (!harness) {
harness = build(hands);
harness = build(hands).then((h) => checkUpFront(name, upFront, h));
built.set(hands, harness);
// A build that failed (a missing credential) is tried again on the next drive.
harness.catch(() => built.delete(hands));
Expand Down
8 changes: 6 additions & 2 deletions apps/demo/lib/harnesses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export function processHarnessOptions(): HarnessOption[] {
});
}

/** The kernel harnesses, each built per hands on its first drive with them (lib/hands.ts `byHands`). */
/**
* The kernel harnesses, each built per hands on its first drive with them
* (lib/hands.ts `byHands`). Each records its own model calls: the CLIs and
* the reference CLI behind the Gateway proxy, the AI SDK from its requests.
*/
export function processKernelHarnesses(): Harness[] {
return processHarnesses().map((h) => byHands(h.value, undefined, (hands) => h.load(hands)));
return processHarnesses().map((h) => byHands(h.value, { modelCalls: 'self' }, (hands) => h.load(hands)));
}
18 changes: 9 additions & 9 deletions apps/demo/lib/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { virtual } from 'durable-harness';
import { vercelChannel } from 'harness-world/vercel-channel';
import type { VercelSandboxProvider } from 'harness-world-hybrid/sandbox';
import { byHands, demoSandbox } from './hands';
import { byHands, checkUpFront, demoSandbox, type UpFront, upFrontFields } from './hands';
import { wakeLane } from './hybrid';
import { getSandboxProvider, providerFrom } from './sandbox-provider';
import { Todos } from './docs';
Expand Down Expand Up @@ -53,14 +53,14 @@ export function preloadHarnesses(): void {
heavy().catch(() => {});
}

/** A harness whose implementation is built on its first drive. `name` and `lanes` are known up front. */
function lazyHarness(name: string, lanes: LaneOptions | undefined, make: (h: Heavy) => Harness): Harness {
/** A harness whose implementation is built on its first drive. `name` and `upFront` are known up front. */
function lazyHarness(name: string, upFront: UpFront, make: (h: Heavy) => Harness): Harness {
let built: Promise<Harness> | undefined;
return {
name,
...(lanes ? { lanes } : {}),
...upFrontFields(upFront),
async drive(ctx) {
const ready = (built ??= heavy().then(make));
const ready = (built ??= heavy().then((h) => checkUpFront(name, upFront, make(h))));
return (await ready).drive(ctx);
},
};
Expand Down Expand Up @@ -97,7 +97,7 @@ async function handsTools(h: Heavy, hands: Hands, onWorkspace: (workspace: Param
}

function pi() {
return byHands('pi', undefined, async (hands) => {
return byHands('pi', { modelCalls: 'self' }, async (hands) => {
const h = await heavy();
const { piAi, gw, piMod } = h;
const models = piAi.createModels();
Expand All @@ -116,7 +116,7 @@ function pi() {
}

function fx() {
return byHands('fx', undefined, async (hands) => {
return byHands('fx', { modelCalls: 'self' }, async (hands) => {
const h = await heavy();
const { fxMod, handsFx, libfx } = h;
return fxMod.fxHarness({
Expand Down Expand Up @@ -191,7 +191,7 @@ function sandboxOwners(): OwnerPolicy {
/** The hybrid Pi session, server half: the tools for each session's hands (`config.hands`), the same list as the tab's. */
function hybrid(world: World) {
const lanes = serverLanes(world);
return lazyHarness(HYBRID_HARNESS, lanes, (h) => {
return lazyHarness(HYBRID_HARNESS, { lanes, modelCalls: 'self' }, (h) => {
const { model, streamFn } = h.hybridMod.createGatewayModel(undefined, MODEL);
return h.hybridMod.hybridHarness({
lane: 'function',
Expand All @@ -211,7 +211,7 @@ function hybrid(world: World) {
*/
function hybridFx(world: World) {
const lanes = serverLanes(world);
return lazyHarness(HYBRID_FX_HARNESS, lanes, (h) =>
return lazyHarness(HYBRID_FX_HARNESS, { lanes, modelCalls: 'self' }, (h) =>
h.hybridMod.hybridFxHarness({
lane: 'function',
createFxEngine: h.libfx.createFxEngine,
Expand Down
Loading