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
3 changes: 2 additions & 1 deletion src/agent-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ export class AgentRuntime {
/** Current step identifier */
stepId: string | null = null;
/** Current step index (0-based) */
stepIndex: number = 0;
// 0-based step indexing (first auto-generated stepId is "step-0")
stepIndex: number = -1;
/** Most recent snapshot (for assertion context) */
lastSnapshot: Snapshot | null = null;
private stepPreSnapshot: Snapshot | null = null;
Expand Down
78 changes: 76 additions & 2 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
import { Page } from 'playwright';

import { AgentRuntime, AttachOptions } from './agent-runtime';
import { AgentRuntime, AssertionHandle, AttachOptions } from './agent-runtime';
import { Predicate } from './verification';
import { Tracer } from './tracing/tracer';

class DebuggerAssertionHandle extends AssertionHandle {
private dbg: SentienceDebugger;
private autoClose: boolean;
private openedStepId: string | null;

constructor(
dbg: SentienceDebugger,
predicate: Predicate,
label: string,
required: boolean,
autoClose: boolean,
openedStepId: string | null
) {
super(dbg.runtime, predicate, label, required);
this.dbg = dbg;
this.autoClose = autoClose;
this.openedStepId = openedStepId;
}

override once(): boolean {
const ok = super.once();
if (this.autoClose && this.dbg.isAutoStepOpenFor(this.openedStepId)) {
void this.dbg.endStep();
}
return ok;
}

override async eventually(
options: Parameters<AssertionHandle['eventually']>[0] = {}
): Promise<boolean> {
const ok = await super.eventually(options);
if (this.autoClose && this.dbg.isAutoStepOpenFor(this.openedStepId)) {
await this.dbg.endStep();
}
return ok;
}
}

export class SentienceDebugger {
readonly runtime: AgentRuntime;
private stepOpen: boolean = false;
private autoStep: boolean = true;
private autoOpenedStep: boolean = false;
private autoOpenedStepId: string | null = null;

constructor(runtime: AgentRuntime, options?: { autoStep?: boolean }) {
this.runtime = runtime;
Expand All @@ -19,13 +59,31 @@ export class SentienceDebugger {
return new SentienceDebugger(runtime);
}

isAutoStepOpenFor(stepId: string | null): boolean {
return Boolean(
this.stepOpen &&
this.autoOpenedStep &&
this.autoOpenedStepId &&
stepId &&
this.autoOpenedStepId === stepId
);
}

beginStep(goal: string, stepIndex?: number): string {
// If we previously auto-opened a verification step, close it before starting a real step.
if (this.stepOpen && this.autoOpenedStep) {
void this.endStep();
this.autoOpenedStep = false;
this.autoOpenedStepId = null;
}
this.stepOpen = true;
return this.runtime.beginStep(goal, stepIndex);
}

async endStep(opts: Parameters<AgentRuntime['emitStepEnd']>[0] = {}): Promise<any> {
this.stepOpen = false;
this.autoOpenedStep = false;
this.autoOpenedStepId = null;
// emitStepEnd is synchronous; wrap to satisfy async/await lint rules.
return await Promise.resolve(this.runtime.emitStepEnd(opts));
}
Expand All @@ -43,7 +101,13 @@ export class SentienceDebugger {
return this.runtime.snapshot(options);
}

async recordAction(action: string, url?: string): Promise<void> {
return await this.runtime.recordAction(action, url);
}

check(predicate: Predicate, label: string, required: boolean = false) {
let didAutoOpen = false;
let openedStepId: string | null = null;
if (!this.stepOpen) {
if (!this.autoStep) {
throw new Error(
Expand All @@ -53,7 +117,17 @@ export class SentienceDebugger {
);
}
this.beginStep(`verify:${label}`);
didAutoOpen = true;
openedStepId = this.runtime.stepId;
this.autoOpenedStep = true;
this.autoOpenedStepId = openedStepId;
}
const base = this.runtime.check(predicate, label, required);
if (!didAutoOpen) {
return base;
}
return this.runtime.check(predicate, label, required);
// Return an auto-closing handle for the common "casual" sidecar usage pattern.
// We still call runtime.check(...) above to keep behavior consistent.
return new DebuggerAssertionHandle(this, predicate, label, required, true, openedStepId);
}
}
8 changes: 4 additions & 4 deletions tests/agent-runtime-assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ describe('AgentRuntime.beginStep() stepId format', () => {
const runtime = new AgentRuntime(browserLike as any, page as any, tracer);

const stepId1 = runtime.beginStep('Step 1');
expect(stepId1).toBe('step-1');
expect(runtime.stepIndex).toBe(1);
expect(stepId1).toBe('step-0');
expect(runtime.stepIndex).toBe(0);

const stepId2 = runtime.beginStep('Step 2');
expect(stepId2).toBe('step-2');
expect(runtime.stepIndex).toBe(2);
expect(stepId2).toBe('step-1');
expect(runtime.stepIndex).toBe(1);
});

it('generates stepId matching explicit stepIndex', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/debugger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('SentienceDebugger', () => {

expect(runtime.beginStep).toHaveBeenCalledWith('verify:has_cart', undefined);
expect(runtime.check).toHaveBeenCalled();
expect(handle).toBe('handle');
expect(typeof (handle as any).once).toBe('function');
});

it('can disable auto-step (strict mode)', () => {
Expand Down
Loading