Skip to content

Commit 3494c4f

Browse files
authored
Merge pull request #168 from SentienceAPI/debugger
Sentience Debugger for any agent
2 parents c9285be + 3c95f9d commit 3494c4f

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

src/agent-runtime.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { Tracer } from './tracing/tracer';
5555
import { TraceEventBuilder } from './utils/trace-event-builder';
5656
import { LLMProvider } from './llm-provider';
5757
import { FailureArtifactBuffer, FailureArtifactsOptions } from './failure-artifacts';
58+
import { SentienceBrowser } from './browser';
5859
import type { ToolRegistry } from './tools/registry';
5960
import {
6061
CaptchaContext,
@@ -69,6 +70,13 @@ interface BrowserLike {
6970
snapshot(page: Page, options?: Record<string, any>): Promise<Snapshot>;
7071
}
7172

73+
export interface AttachOptions {
74+
apiKey?: string;
75+
apiUrl?: string;
76+
toolRegistry?: ToolRegistry;
77+
browser?: BrowserLike;
78+
}
79+
7280
const DEFAULT_CAPTCHA_OPTIONS: Required<Omit<CaptchaOptions, 'handler' | 'resetSession'>> = {
7381
policy: 'abort',
7482
minConfidence: 0.7,
@@ -464,6 +472,30 @@ export class AgentRuntime {
464472
return new AssertionHandle(this, predicate, label, required);
465473
}
466474

475+
/**
476+
* Create AgentRuntime from a raw Playwright Page (sidecar mode).
477+
*/
478+
static fromPlaywrightPage(page: Page, tracer: Tracer, options?: AttachOptions): AgentRuntime {
479+
const browser =
480+
options?.browser ??
481+
((): BrowserLike => {
482+
const sentienceBrowser = SentienceBrowser.fromPage(page, options?.apiKey, options?.apiUrl);
483+
return {
484+
snapshot: async (_page: Page, snapshotOptions?: Record<string, any>) =>
485+
sentienceBrowser.snapshot(snapshotOptions),
486+
};
487+
})();
488+
489+
return new AgentRuntime(browser, page, tracer, options?.toolRegistry);
490+
}
491+
492+
/**
493+
* Sidecar alias for fromPlaywrightPage().
494+
*/
495+
static attach(page: Page, tracer: Tracer, options?: AttachOptions): AgentRuntime {
496+
return AgentRuntime.fromPlaywrightPage(page, tracer, options);
497+
}
498+
467499
/**
468500
* Create a new AgentRuntime.
469501
*

src/debugger.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Page } from 'playwright';
2+
3+
import { AgentRuntime, AttachOptions } from './agent-runtime';
4+
import { Predicate } from './verification';
5+
import { Tracer } from './tracing/tracer';
6+
7+
export class SentienceDebugger {
8+
readonly runtime: AgentRuntime;
9+
private stepOpen: boolean = false;
10+
11+
constructor(runtime: AgentRuntime) {
12+
this.runtime = runtime;
13+
}
14+
15+
static attach(page: Page, tracer: Tracer, options?: AttachOptions): SentienceDebugger {
16+
const runtime = AgentRuntime.fromPlaywrightPage(page, tracer, options);
17+
return new SentienceDebugger(runtime);
18+
}
19+
20+
beginStep(goal: string, stepIndex?: number): string {
21+
this.stepOpen = true;
22+
return this.runtime.beginStep(goal, stepIndex);
23+
}
24+
25+
async endStep(opts: Parameters<AgentRuntime['emitStepEnd']>[0] = {}): Promise<any> {
26+
this.stepOpen = false;
27+
// emitStepEnd is synchronous; wrap to satisfy async/await lint rules.
28+
return await Promise.resolve(this.runtime.emitStepEnd(opts));
29+
}
30+
31+
async step(goal: string, fn: () => Promise<void> | void, stepIndex?: number): Promise<void> {
32+
this.beginStep(goal, stepIndex);
33+
try {
34+
await fn();
35+
} finally {
36+
await this.endStep();
37+
}
38+
}
39+
40+
async snapshot(options?: Record<string, any>) {
41+
return this.runtime.snapshot(options);
42+
}
43+
44+
check(predicate: Predicate, label: string, required: boolean = false) {
45+
if (!this.stepOpen) {
46+
this.beginStep(`verify:${label}`);
47+
}
48+
return this.runtime.check(predicate, label, required);
49+
}
50+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export {
8787
isCollapsed,
8888
} from './verification';
8989
export { AgentRuntime, AssertionHandle, AssertionRecord, EventuallyOptions } from './agent-runtime';
90+
export { SentienceDebugger } from './debugger';
9091
export { RuntimeAgent } from './runtime-agent';
9192
export type { RuntimeStep, StepVerification } from './runtime-agent';
9293
export { parseVisionExecutorAction, executeVisionExecutorAction } from './vision-executor';

tests/agent-runtime-attach.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { AgentRuntime } from '../src/agent-runtime';
2+
import { SentienceBrowser } from '../src/browser';
3+
import { TraceSink } from '../src/tracing/sink';
4+
import { Tracer } from '../src/tracing/tracer';
5+
import { MockPage } from './mocks/browser-mock';
6+
7+
class MockSink extends TraceSink {
8+
emit(): void {
9+
// no-op
10+
}
11+
async close(): Promise<void> {
12+
// no-op
13+
}
14+
getSinkType(): string {
15+
return 'MockSink';
16+
}
17+
}
18+
19+
describe('AgentRuntime.fromPlaywrightPage()', () => {
20+
it('creates runtime using SentienceBrowser.fromPage()', () => {
21+
const sink = new MockSink();
22+
const tracer = new Tracer('test-run', sink);
23+
const page = new MockPage('https://example.com') as any;
24+
const browserLike = {
25+
snapshot: async () => ({
26+
status: 'success',
27+
url: 'https://example.com',
28+
elements: [],
29+
timestamp: 't1',
30+
}),
31+
};
32+
33+
const spy = jest.spyOn(SentienceBrowser, 'fromPage').mockReturnValue(browserLike as any);
34+
35+
const runtime = AgentRuntime.fromPlaywrightPage(page, tracer);
36+
37+
expect(spy).toHaveBeenCalledWith(page, undefined, undefined);
38+
expect(typeof runtime.browser.snapshot).toBe('function');
39+
expect(runtime.page).toBe(page);
40+
41+
spy.mockRestore();
42+
});
43+
44+
it('passes apiKey and apiUrl to SentienceBrowser.fromPage()', () => {
45+
const sink = new MockSink();
46+
const tracer = new Tracer('test-run', sink);
47+
const page = new MockPage('https://example.com') as any;
48+
const browserLike = {
49+
snapshot: async () => ({
50+
status: 'success',
51+
url: 'https://example.com',
52+
elements: [],
53+
timestamp: 't1',
54+
}),
55+
};
56+
57+
const spy = jest.spyOn(SentienceBrowser, 'fromPage').mockReturnValue(browserLike as any);
58+
59+
const runtime = AgentRuntime.fromPlaywrightPage(page, tracer, {
60+
apiKey: 'sk_test',
61+
apiUrl: 'https://api.example.com',
62+
});
63+
64+
expect(spy).toHaveBeenCalledWith(page, 'sk_test', 'https://api.example.com');
65+
expect(typeof runtime.browser.snapshot).toBe('function');
66+
67+
spy.mockRestore();
68+
});
69+
});

tests/debugger.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { AgentRuntime } from '../src/agent-runtime';
2+
import { SentienceDebugger } from '../src/debugger';
3+
import { TraceSink } from '../src/tracing/sink';
4+
import { Tracer } from '../src/tracing/tracer';
5+
import { MockPage } from './mocks/browser-mock';
6+
7+
class MockSink extends TraceSink {
8+
emit(): void {
9+
// no-op
10+
}
11+
async close(): Promise<void> {
12+
// no-op
13+
}
14+
getSinkType(): string {
15+
return 'MockSink';
16+
}
17+
}
18+
19+
describe('SentienceDebugger', () => {
20+
it('attaches via AgentRuntime.fromPlaywrightPage()', () => {
21+
const sink = new MockSink();
22+
const tracer = new Tracer('test-run', sink);
23+
const page = new MockPage('https://example.com') as any;
24+
const runtime = {} as AgentRuntime;
25+
26+
const spy = jest.spyOn(AgentRuntime, 'fromPlaywrightPage').mockReturnValue(runtime);
27+
28+
const dbg = SentienceDebugger.attach(page, tracer, { apiKey: 'sk', apiUrl: 'https://api' });
29+
30+
expect(spy).toHaveBeenCalledWith(page, tracer, { apiKey: 'sk', apiUrl: 'https://api' });
31+
expect(dbg.runtime).toBe(runtime);
32+
33+
spy.mockRestore();
34+
});
35+
36+
it('step() calls beginStep and emitStepEnd', async () => {
37+
const runtime = {
38+
beginStep: jest.fn().mockReturnValue('step-1'),
39+
emitStepEnd: jest.fn().mockResolvedValue({}),
40+
} as unknown as AgentRuntime;
41+
42+
const dbg = new SentienceDebugger(runtime);
43+
44+
await dbg.step('verify-cart', async () => {
45+
// no-op
46+
});
47+
48+
expect(runtime.beginStep).toHaveBeenCalledWith('verify-cart', undefined);
49+
expect(runtime.emitStepEnd).toHaveBeenCalled();
50+
});
51+
52+
it('check() auto-opens a step if missing', () => {
53+
const runtime = {
54+
beginStep: jest.fn().mockReturnValue('step-1'),
55+
check: jest.fn().mockReturnValue('handle'),
56+
} as unknown as AgentRuntime;
57+
58+
const dbg = new SentienceDebugger(runtime);
59+
60+
const handle = dbg.check(
61+
(_ctx: any) => ({ passed: true, reason: '', details: {} }),
62+
'has_cart'
63+
);
64+
65+
expect(runtime.beginStep).toHaveBeenCalledWith('verify:has_cart', undefined);
66+
expect(runtime.check).toHaveBeenCalled();
67+
expect(handle).toBe('handle');
68+
});
69+
});

0 commit comments

Comments
 (0)