diff --git a/src/agent-runtime.ts b/src/agent-runtime.ts index 5fdacdc..d2edbd0 100644 --- a/src/agent-runtime.ts +++ b/src/agent-runtime.ts @@ -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; diff --git a/src/debugger.ts b/src/debugger.ts index 7a58b18..bea3bec 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -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[0] = {} + ): Promise { + 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; @@ -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[0] = {}): Promise { 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)); } @@ -43,7 +101,13 @@ export class SentienceDebugger { return this.runtime.snapshot(options); } + async recordAction(action: string, url?: string): Promise { + 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( @@ -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); } } diff --git a/tests/agent-runtime-assertions.test.ts b/tests/agent-runtime-assertions.test.ts index 0948d43..4069b09 100644 --- a/tests/agent-runtime-assertions.test.ts +++ b/tests/agent-runtime-assertions.test.ts @@ -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', () => { diff --git a/tests/debugger.test.ts b/tests/debugger.test.ts index 3c2f189..11bc4bf 100644 --- a/tests/debugger.test.ts +++ b/tests/debugger.test.ts @@ -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)', () => {