Skip to content
Open
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
3 changes: 1 addition & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
}
},
"linter": {
"enabled": true,
"preset": "recommended"
"enabled": true
}
}
38 changes: 25 additions & 13 deletions src/__tests__/mod.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// jev-nudge mod tests — the wiring: dormant without a key, the request it sends, the
// continue it returns, the per-run bookkeeping, and that Jev failures never change a stop.

import {describe, expect, it} from 'vitest';
import type {JevFetch} from '../client.js';
import {createJevNudgeMod} from '../index.js';
import {NUDGE_REASON, SYSTEM_ONE_PATH} from '../protocol.js';
import type {
AgentState,
ModApi,
ModFlagDefinition,
ModHooks,
} from '../types.js';
import {describe, expect, it} from 'vitest';
import type {JevFetch} from '../client.js';
import {createJevNudgeMod} from '../index.js';
import {NUDGE_REASON, SYSTEM_ONE_PATH} from '../protocol.js';

interface FakeHost {
readonly cmd: ModApi;
Expand Down Expand Up @@ -123,9 +123,11 @@ async function stop(
describe('jev-nudge mod', () => {
it('registers only its flags without a key, and says so', () => {
const host = fakeCmd();
createJevNudgeMod({env: {}, storedKey: noStoredKey, fetch: fakeFetch(() => answers(1)).fetch})(
host.cmd,
);
createJevNudgeMod({
env: {},
storedKey: noStoredKey,
fetch: fakeFetch(() => answers(1)).fetch,
})(host.cmd);
expect([...host.flags.keys()]).toEqual([
'jev-nudge-threshold',
'jev-nudge-max',
Expand Down Expand Up @@ -223,8 +225,11 @@ describe('jev-nudge mod', () => {
it('only judges natural end_turn stops', async () => {
const host = fakeCmd();
const {fetch, captured} = fakeFetch(() => answers(1));
createJevNudgeMod({env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey, fetch})(host.cmd);
createJevNudgeMod({
env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey,
fetch,
})(host.cmd);
expect(await stop(host, {stopReason: 'max_tokens'})).toBeUndefined();
expect(captured.requests).toEqual([]);
});
Expand All @@ -234,7 +239,7 @@ describe('jev-nudge mod', () => {
const host = fakeCmd();
createJevNudgeMod({
env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey,
storedKey: noStoredKey,
fetch: fakeFetch(() => failure).fetch,
})(host.cmd);
expect(await stop(host)).toBeUndefined();
Expand All @@ -247,8 +252,11 @@ describe('jev-nudge mod', () => {
const host = fakeCmd();
host.flagValues.set('jev-nudge-max', '2');
const {fetch, captured} = fakeFetch(() => answers(0.9, 0.1, 0.9));
createJevNudgeMod({env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey, fetch})(host.cmd);
createJevNudgeMod({
env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey,
fetch,
})(host.cmd);
const hooks = host.hooks[0];
if (!hooks?.afterToolCall || !hooks.onTurnStart)
throw new Error('hooks missing');
Expand Down Expand Up @@ -309,7 +317,11 @@ describe('jev-nudge mod', () => {
host.flagValues.set('jev-nudge-model', 'jev-2');
host.flagValues.set('jev-nudge-threshold', '0.95');
const {fetch, captured} = fakeFetch(() => answers(0.9));
createJevNudgeMod({env: {CMD_API_KEY: 'ck'}, storedKey: noStoredKey, fetch})(host.cmd);
createJevNudgeMod({
env: {CMD_API_KEY: 'ck'},
storedKey: noStoredKey,
fetch,
})(host.cmd);

expect(await stop(host)).toBeUndefined();
expect(captured.requests[0]?.url).toBe(
Expand Down
34 changes: 17 additions & 17 deletions src/__tests__/protocol.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// jev-nudge protocol tests — the pure half: key resolution, the state Jev sees, the
// questions asked, and how probabilities become a stop-hook decision.

import type {AgentMessage} from '../types.js';
import {describe, expect, it} from 'vitest';
import {
buildNudgeState,
COMMAND_CODE_BASE_URL,
DEFAULT_MAX_NUDGES,
DEFAULT_THRESHOLD,
decideNudge,
COMMAND_CODE_BASE_URL,
JEV_MODEL,
jevResponseSchema,
nudgeQuestions,
parseMaxNudges,
parseThreshold,
JEV_MODEL,
resolveJevConfig,
} from '../protocol.js';
import type {AgentMessage} from '../types.js';

describe('resolveJevConfig', () => {
it('is dormant without a key', () => {
Expand All @@ -37,8 +37,10 @@ describe('resolveJevConfig', () => {
model: JEV_MODEL,
});
expect(
resolveJevConfig({env: {COMMAND_CODE_API_KEY: 'cli'}, storedKey: 'stored'})
?.apiKey,
resolveJevConfig({
env: {COMMAND_CODE_API_KEY: 'cli'},
storedKey: 'stored',
})?.apiKey,
).toBe('cli');
expect(resolveJevConfig({env: {}, storedKey: 'stored'})?.apiKey).toBe(
'stored',
Expand Down Expand Up @@ -132,19 +134,17 @@ describe('buildNudgeState', () => {

describe('nudgeQuestions', () => {
it('asks the progress guard only once a nudge already happened', () => {
expect(Object.keys(nudgeQuestions({hasPreviousNudge: false}))).toEqual([
'nudge',
'waiting',
]);
expect(Object.keys(nudgeQuestions({hasPreviousNudge: true}))).toEqual([
'nudge',
'waiting',
'progress',
]);
expect(
Object.keys(
nudgeQuestions({hasPreviousNudge: false}),
),
).toEqual(['nudge', 'waiting']);
expect(
Object.keys(
nudgeQuestions({hasPreviousNudge: true}),
),
).toEqual(['nudge', 'waiting', 'progress']);
expect(
nudgeQuestions({hasPreviousNudge: false}).nudge
.instructions,
nudgeQuestions({hasPreviousNudge: false}).nudge.instructions,
).toContain('gentle nudge');
});

Expand Down
4 changes: 1 addition & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ export async function askJev(params: AskJevParams): Promise<AskJevResult> {
`jev ${response.status}${text ? `: ${text.slice(0, 200)}` : ''}`,
);
}
const parsed = jevResponseSchema.safeParse(
await response.json(),
);
const parsed = jevResponseSchema.safeParse(await response.json());
if (!parsed.success) {
throw new Error(
`jev returned an unexpected shape: ${parsed.error.message}`,
Expand Down
21 changes: 12 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@
import {readFileSync} from 'node:fs';
import {homedir} from 'node:os';
import {join} from 'node:path';
import type {ModApi} from './types.js';
import {askJev, type JevFetch} from './client.js';
import {
buildNudgeState,
CMD_KEY_ENV,
DEFAULT_THRESHOLD,
decideNudge,
JEV_NUDGE_MOD_NAME,
NUDGE_REASON,
nudgeQuestions,
type PreviousNudge,
parseMaxNudges,
parseThreshold,
type PreviousNudge,
resolveJevConfig,
CMD_KEY_ENV,
} from './protocol.js';
import type {ModApi} from './types.js';

// A stop decision is on the critical path between "model finished" and "prompt comes back";
// a gut-check model should answer well inside this, and past it the run just stops.
Expand Down Expand Up @@ -77,13 +77,11 @@ export function createJevNudgeMod(
});
cmd.addFlag('jev-nudge-base-url', {
type: 'string',
description:
'API root for Jev (default: https://api.commandcode.ai)',
description: 'API root for Jev (default: https://api.commandcode.ai)',
});
cmd.addFlag('jev-nudge-model', {
type: 'string',
description:
'Model id sent to the endpoint (default: typesafe/jev)',
description: 'Model id sent to the endpoint (default: typesafe/jev)',
});

cmd.addFlag('jev-nudge-verbose', {
Expand Down Expand Up @@ -164,7 +162,8 @@ export function createJevNudgeMod(
} catch (error) {
// Best-effort: the stop decision must never depend on a network call succeeding,
// but a failing call is said out loud so "no nudge" is never a mystery.
const message = error instanceof Error ? error.message : String(error);
const message =
error instanceof Error ? error.message : String(error);
dlog(`[${JEV_NUDGE_MOD_NAME}] ${message}`);
cmd.ui.notify(
`${JEV_NUDGE_MOD_NAME}: Jev call failed (${message}), letting the run stop`,
Expand All @@ -185,7 +184,11 @@ export function createJevNudgeMod(
run = {
nudges: [
...previousNudges,
{turnNumber, toolCallsAfter: 0, assistantTextAtNudge: lastAssistantText},
{
turnNumber,
toolCallsAfter: 0,
assistantTextAtNudge: lastAssistantText,
},
],
toolCallsSinceNudge: 0,
};
Expand Down
6 changes: 4 additions & 2 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// probabilities instead of generating text, so the "should the agent keep going?" judgment
// comes back as numbers this file can threshold, with no prose to parse.

import type {AgentMessage} from './types.js';
import {z} from 'zod';
import type {AgentMessage} from './types.js';

export const JEV_NUDGE_MOD_NAME = 'jev-nudge';

Expand Down Expand Up @@ -244,7 +244,9 @@ export interface NudgeAnswers {

// System One returns `{model, answers, usage}` with each yes/no answered as `{type: 'noul',
// noul}`; the schema hands back plain numbers so nothing downstream sees the wire shape.
const probability = z.object({noul: z.number()}).transform(answer => answer.noul);
const probability = z
.object({noul: z.number()})
.transform(answer => answer.noul);

export const jevResponseSchema = z.object({
model: z.string().optional(),
Expand Down