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
1 change: 1 addition & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ Raw CSS content to be injected into frame.

## async method: Page.agent
* since: v1.58
* langs: js
- returns: <[PageAgent]>

Initialize page agent with the llm provider and cache.
Expand Down
17 changes: 17 additions & 0 deletions docs/src/api/class-pageagent.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# class: PageAgent
* since: v1.58
* langs: js

## event: PageAgent.turn
* since: v1.58
Expand Down Expand Up @@ -95,3 +96,19 @@ Task to perform using agentic loop.

### option: PageAgent.perform.-inline- = %%-page-agent-call-options-v1.58-%%
* since: v1.58

## async method: PageAgent.usage
* since: v1.58
- returns: <[Object]>
- `turns` <[int]>
- `inputTokens` <[int]>
- `outputTokens` <[int]>

Returns the current token usage for this agent.

**Usage**

```js
const usage = await agent.usage();
console.log(`Tokens used: ${usage.inputTokens} in, ${usage.outputTokens} out`);
```
19 changes: 19 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5462,6 +5462,25 @@ export interface PageAgent {
};
}>;

/**
* Returns the current token usage for this agent.
*
* **Usage**
*
* ```js
* const usage = await agent.usage();
* console.log(`Tokens used: ${usage.inputTokens} in, ${usage.outputTokens} out`);
* ```
*
*/
usage(): Promise<{
turns: number;

inputTokens: number;

outputTokens: number;
}>;

[Symbol.asyncDispose](): Promise<void>;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
{
"name": "webkit",
"revision": "2245",
"revision": "2248",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/client/pageAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export class PageAgent extends ChannelOwner<channels.PageAgentChannel> implement
return { result, usage };
}

async usage() {
const { usage } = await this._channel.usage({});
return usage;
}

async dispose() {
await this._channel.dispose();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,10 @@ scheme.PageAgentExtractResult = tObject({
});
scheme.PageAgentDisposeParams = tOptional(tObject({}));
scheme.PageAgentDisposeResult = tOptional(tObject({}));
scheme.PageAgentUsageParams = tOptional(tObject({}));
scheme.PageAgentUsageResult = tObject({
usage: tType('AgentUsage'),
});
scheme.AgentUsage = tObject({
turns: tInt,
inputTokens: tInt,
Expand Down
214 changes: 120 additions & 94 deletions packages/playwright-core/src/server/agent/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,97 +14,123 @@
* limitations under the License.
*/

export type NavigateAction = {
method: 'navigate';
url: string;
};

export type ClickAction = {
method: 'click';
selector: string;
button?: 'left' | 'right' | 'middle';
clickCount?: number;
modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
};

export type DragAction = {
method: 'drag';
sourceSelector: string;
targetSelector: string;
};

export type HoverAction = {
method: 'hover';
selector: string;
modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
};

export type SelectOptionAction = {
method: 'selectOption';
selector: string;
labels: string[];
};

export type PressAction = {
method: 'pressKey';
// Includes modifiers
key: string;
};

export type PressSequentiallyAction = {
method: 'pressSequentially';
selector: string;
text: string;
submit?: boolean;
};

export type FillAction = {
method: 'fill';
selector: string;
text: string;
submit?: boolean;
};

export type SetChecked = {
method: 'setChecked';
selector: string;
checked: boolean;
};

export type ExpectVisible = {
method: 'expectVisible';
selector: string;
isNot?: boolean;
};

export type ExpectValue = {
method: 'expectValue';
selector: string;
type: 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'slider';
value: string;
isNot?: boolean;
};

export type ExpectAria = {
method: 'expectAria';
template: string;
isNot?: boolean;
};

export type Action =
| NavigateAction
| ClickAction
| DragAction
| HoverAction
| SelectOptionAction
| PressAction
| PressSequentiallyAction
| FillAction
| SetChecked
| ExpectVisible
| ExpectValue
| ExpectAria;

export type ActionWithCode = Action & {
code: string;
};
import { zod } from '../../utilsBundle';
import type z from 'zod';

const modifiersSchema = zod.array(
zod.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])
);

const navigateActionSchema = zod.object({
method: zod.literal('navigate'),
url: zod.string(),
});
export type NavigateAction = z.infer<typeof navigateActionSchema>;

const clickActionSchema = zod.object({
method: zod.literal('click'),
selector: zod.string(),
button: zod.enum(['left', 'right', 'middle']).optional(),
clickCount: zod.number().optional(),
modifiers: modifiersSchema.optional(),
});
export type ClickAction = z.infer<typeof clickActionSchema>;

const dragActionSchema = zod.object({
method: zod.literal('drag'),
sourceSelector: zod.string(),
targetSelector: zod.string(),
});
export type DragAction = z.infer<typeof dragActionSchema>;

const hoverActionSchema = zod.object({
method: zod.literal('hover'),
selector: zod.string(),
modifiers: modifiersSchema.optional(),
});
export type HoverAction = z.infer<typeof hoverActionSchema>;

const selectOptionActionSchema = zod.object({
method: zod.literal('selectOption'),
selector: zod.string(),
labels: zod.array(zod.string()),
});
export type SelectOptionAction = z.infer<typeof selectOptionActionSchema>;

const pressActionSchema = zod.object({
method: zod.literal('pressKey'),
key: zod.string(),
});
export type PressAction = z.infer<typeof pressActionSchema>;

const pressSequentiallyActionSchema = zod.object({
method: zod.literal('pressSequentially'),
selector: zod.string(),
text: zod.string(),
submit: zod.boolean().optional(),
});
export type PressSequentiallyAction = z.infer<typeof pressSequentiallyActionSchema>;

const fillActionSchema = zod.object({
method: zod.literal('fill'),
selector: zod.string(),
text: zod.string(),
submit: zod.boolean().optional(),
});
export type FillAction = z.infer<typeof fillActionSchema>;

const setCheckedSchema = zod.object({
method: zod.literal('setChecked'),
selector: zod.string(),
checked: zod.boolean(),
});
export type SetChecked = z.infer<typeof setCheckedSchema>;

const expectVisibleSchema = zod.object({
method: zod.literal('expectVisible'),
selector: zod.string(),
isNot: zod.boolean().optional(),
});
export type ExpectVisible = z.infer<typeof expectVisibleSchema>;

const expectValueSchema = zod.object({
method: zod.literal('expectValue'),
selector: zod.string(),
type: zod.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']),
value: zod.string(),
isNot: zod.boolean().optional(),
});
export type ExpectValue = z.infer<typeof expectValueSchema>;

const expectAriaSchema = zod.object({
method: zod.literal('expectAria'),
template: zod.string(),
isNot: zod.boolean().optional(),
});
export type ExpectAria = z.infer<typeof expectAriaSchema>;

const actionSchema = zod.discriminatedUnion('method', [
navigateActionSchema,
clickActionSchema,
dragActionSchema,
hoverActionSchema,
selectOptionActionSchema,
pressActionSchema,
pressSequentiallyActionSchema,
fillActionSchema,
setCheckedSchema,
expectVisibleSchema,
expectValueSchema,
expectAriaSchema,
]);
export type Action = z.infer<typeof actionSchema>;

const actionWithCodeSchema = actionSchema.and(zod.object({
code: zod.string(),
}));
export type ActionWithCode = z.infer<typeof actionWithCodeSchema>;

export const cachedActionsSchema = zod.record(zod.object({
actions: zod.array(actionWithCodeSchema),
}));
export type CachedActions = z.infer<typeof cachedActionsSchema>;
17 changes: 8 additions & 9 deletions packages/playwright-core/src/server/agent/pageAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Context } from './context';
import performTools from './performTools';
import expectTools from './expectTools';

import type * as actions from './actions';
import * as actions from './actions';
import type { ToolDefinition } from './tool';
import type * as loopTypes from '@lowire/loop';
import type { Progress } from '../progress';
Expand Down Expand Up @@ -151,10 +151,6 @@ async function runLoop(progress: Progress, context: Context, toolDefinitions: To
return { result: resultSchema ? reportedResult() : undefined };
}

type CachedActions = Record<string, {
actions: actions.ActionWithCode[],
}>;

async function cachedPerform(progress: Progress, context: Context, cacheKey: string): Promise<actions.ActionWithCode[] | undefined> {
if (!context.agentParams?.cacheFile)
return;
Expand Down Expand Up @@ -191,17 +187,20 @@ async function updateCache(context: Context, cacheKey: string) {
}

type Cache = {
actions: CachedActions;
newActions: CachedActions;
actions: actions.CachedActions;
newActions: actions.CachedActions;
};

const allCaches = new Map<string, Cache>();

async function cachedActions(cacheFile: string): Promise<Cache> {
let cache = allCaches.get(cacheFile);
if (!cache) {
const actions = await fs.promises.readFile(cacheFile, 'utf-8').then(text => JSON.parse(text)).catch(() => ({})) as CachedActions;
cache = { actions, newActions: {} };
const text = await fs.promises.readFile(cacheFile, 'utf-8').catch(() => '{}');
const parsed = actions.cachedActionsSchema.safeParse(JSON.parse(text));
if (parsed.error)
throw new Error(`Failed to parse cache file ${cacheFile}: ${parsed.error.issues.map(issue => issue.message).join(', ')}`);
cache = { actions: parsed.data, newActions: {} };
allCaches.set(cacheFile, cache);
}
return cache;
Expand Down
Loading
Loading