Compare commits

..

173 Commits

Author SHA1 Message Date
Simon 299a5b2075 chore(version): bump version to 1.0.0 2026-01-22 15:38:21 +08:00
Simon 785a2d194e chore(version): bump version to 1.0.0-beta.5 2026-01-22 15:17:19 +08:00
Simon 391e70dca4 feat: update historical event types 2026-01-22 15:16:15 +08:00
Simon 3ffcd6482d refactor: rename AgentStep to AgentStepEventErrorEvent to AgentErrorEvent 2026-01-22 14:59:35 +08:00
Simon 0cd04db40f fix: rebuild lock 2026-01-22 14:12:37 +08:00
Simon 23f1818503 docs: update README for clarity and integration details 2026-01-21 20:15:33 +08:00
Simon 5fdb49fecb fix(sync-version): update version check to use pkgJson.version 2026-01-21 19:20:23 +08:00
Simon fe1893a382 fix: rebuild lock 2026-01-21 19:04:26 +08:00
Simon 8c1c1ed9fe fix: correct prettier extension recommendation 2026-01-21 19:01:27 +08:00
Simon 6cbb4e99c4 chore(version): bump version to 1.0.0-beta.4 2026-01-21 18:55:07 +08:00
Simon 34ce56b446 feat(PageController): make sure page is indexed before calling actions on elements 2026-01-21 18:54:17 +08:00
Simon 23ec4602c2 feat: export types from './types' in PageAgentCore 2026-01-21 17:26:31 +08:00
Simon 536e5076da chore(version): bump version to 1.0.0-beta.3 2026-01-21 17:00:08 +08:00
Simon 81807e17ca feat: add stepIndex to history events 2026-01-21 16:59:22 +08:00
Simon 6944bedf85 chore: correct formatting in markdown 2026-01-21 01:30:27 +08:00
Simon 897b919044 chore(version): bump version to 1.0.0-beta.2 2026-01-21 01:23:42 +08:00
Simon 3a3bb0e58c fix: use spaces for JSON formatting in version sync script 2026-01-21 01:23:07 +08:00
Simon bc7ccf47ff fix: handle version in sync script 2026-01-21 01:15:58 +08:00
Simon c781eba986 feat: fixed version in CDN URL 2026-01-21 01:08:19 +08:00
Simon efe131660a fix: demo not showing panel; docs updated 2026-01-21 00:44:09 +08:00
Simon 22516dec74 feat: unify InvokeError; add rawResponse to historical ErrorEvent 2026-01-20 20:16:34 +08:00
Simon 1f8953c450 feat: add llm rawResponse to history 2026-01-20 19:56:40 +08:00
Simon d6a57f3e20 chore: merge .gitignore from feat/ext 2026-01-20 19:53:52 +08:00
Simon e0027f99c4 Merge pull request #121 from alibaba/dependabot/npm_and_yarn/development-dependencies-0b42ca76aa
chore(deps-dev): bump the development-dependencies group with 6 updates
2026-01-20 14:14:40 +08:00
Simon b234868bc1 Merge pull request #120 from alibaba/dependabot/npm_and_yarn/production-dependencies-f96e18e569
chore(deps): bump the production-dependencies group with 2 updates
2026-01-20 14:13:47 +08:00
dependabot[bot] 10ae34a677 chore(deps-dev): bump the development-dependencies group with 6 updates
Bumps the development-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.7` | `25.0.9` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.7` | `2.7.2` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.7` | `2.7.2` |
| [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.53.0` | `8.53.1` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.2` | `16.5.3` |


Updates `@types/node` from 25.0.7 to 25.0.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint-plugin-react-dom` from 2.5.7 to 2.7.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.7.2/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.5.7 to 2.7.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.7.2/packages/plugins/eslint-plugin-react-x)

Updates `prettier` from 3.7.4 to 3.8.0
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.0)

Updates `typescript-eslint` from 8.53.0 to 8.53.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.53.1/packages/typescript-eslint)

Updates `react-i18next` from 16.5.2 to 16.5.3
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.2...v16.5.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.7.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.7.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 06:11:50 +00:00
dependabot[bot] 2fb01b7315 chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.26.1 to 12.27.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.26.1...v12.27.1)

Updates `simple-icons` from 16.5.0 to 16.6.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.5.0...16.6.0)

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.27.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 06:11:18 +00:00
Simon 5c80f2b002 chore: prettier json indentation 2026-01-20 14:08:42 +08:00
Simon 28c128645e chore(version): bump version to 1.0.0-beta.1 2026-01-19 20:51:37 +08:00
Simon 250cb47e44 fix: wait time second not ms 2026-01-19 20:50:50 +08:00
Simon f83a4174cc chore(version): bump version to 1.0.0-beta.0 2026-01-19 20:39:57 +08:00
Simon 334512210d feat: improve lifecycle hooks API; wait a little after url change 2026-01-19 20:39:03 +08:00
Simon 3df71788ff docs: update package descriptions 2026-01-19 20:14:40 +08:00
Simon 60450ea7be chore: export tool from PageAgent package 2026-01-19 20:12:46 +08:00
Simon 80c93bd668 feat: expose maxSteps config 2026-01-19 20:11:53 +08:00
Simon 6d53f4cb44 chore(version): bump version to 0.3.0 2026-01-19 19:47:21 +08:00
Simon b35489d6a5 chore(version): bump version to 0.3.0-beta.2 2026-01-19 19:41:57 +08:00
Simon d78cbb6b6e chore(release): enhance npm publish for prerelease tags 2026-01-19 19:41:39 +08:00
Simon e9c5cece4f docs: update CDN URLs in README files 2026-01-19 19:39:13 +08:00
Simon 4b69a7ee00 chore(version): bump version to 0.3.0-beta.1 2026-01-19 19:35:27 +08:00
Simon 25e4d07fc5 chore(demo): clean up 2026-01-19 19:33:15 +08:00
Simon 6a381dede1 chore(version): bump version to 0.3.0-beta 2026-01-19 19:31:45 +08:00
Simon b3bfbb96bb docs: update cdn urls 2026-01-19 19:31:07 +08:00
Simon 3d10953324 feat: remove iife-full build 2026-01-19 19:18:19 +08:00
Simon 8dd4c5d78e feat: remove @page-agent/cdn 2026-01-19 19:10:00 +08:00
Simon 8bde218f28 Merge pull request #119 from alibaba/feat/decouple-dom
feat!: decouple `DOM` from `Agent`
2026-01-19 17:22:57 +08:00
Simon 0643cc4ff9 feat: enhance headless mode documentation and usage 2026-01-19 17:14:36 +08:00
Simon 4c5dd23fcd feat: decouple PageController from PageAgent 2026-01-19 17:14:09 +08:00
Simon 3ce4d8e3fe docs: update instructions. add advanced section 2026-01-19 16:47:48 +08:00
Simon b217e6a2ca feat!: PageAgent with UI by default 2026-01-19 16:07:17 +08:00
Simon c9f049a733 refactor: rename page-agent to page-agent-core 2026-01-19 16:06:07 +08:00
Simon 09c3084629 chore: typo 2026-01-19 15:21:43 +08:00
Simon 38c8020e4a chore: typo 2026-01-19 15:15:23 +08:00
Simon 5057f7472b feat: pass language to formatTime for localization 2026-01-19 14:25:25 +08:00
Simon 6820eec676 feat(PageController): make showMask and hideMask async 2026-01-19 12:54:33 +08:00
Simon ef2777b521 feat: custom PageController; remove beforeunload; node-safe PageController 2026-01-18 00:07:23 +08:00
Simon 8bba3a63f5 chore: ui folder structure 2026-01-17 23:35:07 +08:00
Simon 722eb10286 feat: change onAfterStep params 2026-01-17 23:31:02 +08:00
Simon 15ffc27059 chore: agent event docs 2026-01-17 23:30:37 +08:00
Simon 5f162b1a1c chore: rm event bus; separate agent types 2026-01-17 23:30:12 +08:00
Simon 083c002ce0 fix: type 2026-01-17 23:20:09 +08:00
Simon 1ede1d9911 feat!: redo Panel; decouple Panel from Agent
BREAKING CHANGES: Agent and Panel API Changes
2026-01-17 23:14:26 +08:00
Simon 8abe3afed9 fix: type 2026-01-17 18:07:59 +08:00
Simon af36f43104 chore(version): bump version to 0.2.5 2026-01-17 17:47:50 +08:00
Simon b5f260207c chore: wording 2026-01-17 17:47:04 +08:00
Simon 9075204c29 Merge pull request #118 from alibaba/feat/make-panel-optional
feat: make all ui optional
2026-01-17 17:43:40 +08:00
Simon ce3a80bc53 docs: typo 2026-01-17 17:30:16 +08:00
Simon 64c36f67bb docs: update config; instructions for programmatic usage 2026-01-17 17:28:28 +08:00
Simon 78d4919289 feat: make panel optional 2026-01-17 17:24:17 +08:00
Simon 1af2607cca feat: remove ask_user tool from sys_prompt 2026-01-17 17:18:31 +08:00
Simon ab713f3d3a chore(version): bump version to 0.2.4 2026-01-17 16:22:45 +08:00
Simon 05cc4f36c8 chore: add .prettierignore for system_prompt.md 2026-01-17 16:22:28 +08:00
Simon 54e9bdad7d chore: prettier json and markdowns 2026-01-17 16:15:36 +08:00
Simon 0fc88c0080 chore(version): bump version to 0.2.3 2026-01-17 16:03:11 +08:00
Simon 02f97ef2fa chore: banner 2026-01-17 15:57:22 +08:00
Simon 0853ffbbc6 chore: banner 2026-01-17 15:48:30 +08:00
Simon 60ac6da09f chore: new demo video 2026-01-17 15:17:14 +08:00
Simon 11a9fb1181 chore: new demo video 2026-01-17 15:15:48 +08:00
Simon 3762d0f1e0 chore: refine docs 2026-01-17 14:59:09 +08:00
Simon ebbe92f7ca fix(website): update links to use relative paths 2026-01-17 02:13:19 +08:00
Simon 9f239b2de3 chore: comment out unused config 2026-01-17 02:05:30 +08:00
Simon 2410c27b55 chore(website): update config docs 2026-01-17 02:05:09 +08:00
Simon 6d6c82648c chore: cleanup 2026-01-17 01:46:35 +08:00
Simon f41119c1ad fix(website): fix router 2026-01-17 01:36:15 +08:00
Simon 9e6e1b996c chore(website): simplify chunks 2026-01-17 01:27:19 +08:00
Simon 1ab23e181f chore: clean up 2026-01-17 01:23:23 +08:00
Simon a1b485691f chore(version): bump version to 0.2.2 2026-01-17 00:59:55 +08:00
Simon 694e05b800 chore: npm audit fix 2026-01-17 00:54:20 +08:00
Simon b761795c1a feat!: local iife testing
BREAKING CHANGES: remove umd build from npm package and cdn
2026-01-16 19:04:26 +08:00
Simon 8cc54983aa chore(version): bump version to 0.2.1 2026-01-16 16:16:18 +08:00
Simon 59a599285e Merge pull request #115 from alibaba/fix/phantom-panel
fix(panel): hide panel by default
2026-01-16 16:15:43 +08:00
Simon 2f8c2b395c fix(panel): hide panel by default 2026-01-16 16:06:57 +08:00
Simon 28f836674c feat: edge feedback for scroll actions 2026-01-16 15:53:47 +08:00
Simon 6f01632231 fix(website): build error 2026-01-16 15:09:30 +08:00
Simon 41123ac561 chore(website): optimize bundle size 2026-01-16 15:05:09 +08:00
Simon c8bf0dd20f chore: get rid of build warnings 2026-01-16 14:50:56 +08:00
Simon ec2b744bab Merge pull request #114 from alibaba/feat/user-takeover
feat: remove `pause`
2026-01-15 21:51:01 +08:00
Simon eab9cf64e8 feat: includes history events in panel 2026-01-15 21:48:14 +08:00
Simon 42130d6f1d feat: remove pause 2026-01-15 21:37:03 +08:00
Simon c0ce6e7d14 chore(version): bump version to 0.2.0 2026-01-15 20:47:39 +08:00
Simon 8f5f98f889 feat(agent)!: rename brain to reflection; fix undefined lines in panel 2026-01-15 20:45:40 +08:00
Simon ee4719cdcf feat: observation 2026-01-15 20:28:39 +08:00
Simon b4c82b7833 refactor: mv getBrowserState to Controller; simplify Agent 2026-01-15 20:17:50 +08:00
Simon aefc3cfb89 Merge pull request #111 from alibaba/feat/extend-agent-history-for-observation
feat: extend history type
2026-01-15 19:57:41 +08:00
Simon 7f2b8c2766 feat: extend history type 2026-01-15 19:18:25 +08:00
Simon 62e24a3050 chore(version): bump version to 0.1.0 2026-01-14 18:50:06 +08:00
Simon 4f6249a252 feat!: require user to provide LLM api. do not fallback to demo LLM
BREAKING CHANGE: LLM API will be required for agent constructor
2026-01-14 18:49:23 +08:00
Simon 62519416bb chore(version): bump version to 0.0.24 2026-01-14 18:34:55 +08:00
Simon a1e70e663d feat(PageController): exclude aria-hidden elements 2026-01-14 18:04:31 +08:00
Simon 338e317be1 chore(website): fix homepage overflow 2026-01-14 17:45:49 +08:00
Simon 9a66d5205f feat(website): display version in header and load from page-agent package 2026-01-14 17:42:18 +08:00
Simon 7d45bf6598 feat(website): update CDN links and usage 2026-01-14 17:27:35 +08:00
Simon 78ee72eea4 chore(version): bump version to 0.0.23 2026-01-14 15:46:10 +08:00
Simon 3b71c535b1 fix: clean up debug code 2026-01-14 15:23:02 +08:00
Simon 6615f7b300 Merge pull request #110 from alibaba/fix/panel-text-uncompleted
feat: add HTML escaping to Panel
2026-01-14 15:17:43 +08:00
Simon 38caae5e23 feat: add HTML escaping to Panel 2026-01-14 15:15:51 +08:00
Simon 1751ba325d chore(version): bump version to 0.0.22 2026-01-13 21:01:16 +08:00
Simon 066b045822 Merge pull request #108 from alibaba/feat/dual-umd
feat: add `cdn` packages for iife builds
2026-01-13 20:58:37 +08:00
Simon 3b21c42cdb feat: add cdn packages for iife builds 2026-01-13 20:52:41 +08:00
Simon 8b1e91d665 Merge pull request #107 from alibaba/refactor/mv-simulator-mask-to-page-controller
Refactor: move simulator mask to page controller
2026-01-13 17:38:37 +08:00
Simon aa5909264a chore: mv mask code to folder 2026-01-13 17:35:24 +08:00
Simon b6232d4e21 refactor: mv SimulatorMask from ui to page-controller 2026-01-13 17:33:38 +08:00
Simon 0d3cef5578 chore(version): bump version to 0.0.21 2026-01-13 15:18:50 +08:00
Simon 1e737f3a71 docs: models recommendation 2026-01-13 15:17:43 +08:00
Simon 2f2bdb4697 fix: update Qwen patch condition for model name 2026-01-13 15:07:01 +08:00
Simon 3fa7c8b85a feat: improve qwen3 compatibility (#106)
* feat: improve qwen3 compatibility

* fix: qwen

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 15:04:03 +08:00
dependabot[bot] 7efce91a91 chore(deps-dev): bump the development-dependencies group with 11 updates (#101)
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.3.0` | `20.3.1` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.3.0` | `20.3.1` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.1` | `6.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.6` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.1` | `2.5.5` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.1` | `2.5.5` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.52.0` | `8.53.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [i18next](https://github.com/i18next/i18next) | `25.7.3` | `25.7.4` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.1` | `16.5.2` |


Updates `@commitlint/cli` from 20.3.0 to 20.3.1
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.3.0 to 20.3.1
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.1...v6.0.2)

Updates `@types/node` from 25.0.3 to 25.0.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint-plugin-react-dom` from 2.5.1 to 2.5.5
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.5/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.5.1 to 2.5.5
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.5/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.52.0 to 8.53.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.53.0/packages/typescript-eslint)

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

Updates `@types/react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `i18next` from 25.7.3 to 25.7.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.7.3...v25.7.4)

Updates `react-i18next` from 16.5.1 to 16.5.2
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.1...v16.5.2)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:30:02 +08:00
dependabot[bot] a11d370f2b chore(deps): bump the production-dependencies group with 3 updates (#100)
Bumps the production-dependencies group with 3 updates: [ai-motion](https://github.com/gaomeng1900/ai-motion), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `ai-motion` from 0.4.7 to 0.4.8
- [Release notes](https://github.com/gaomeng1900/ai-motion/releases)
- [Commits](https://github.com/gaomeng1900/ai-motion/compare/v0.4.7...v0.4.8)

Updates `motion` from 12.24.0 to 12.26.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.24.0...v12.26.1)

Updates `simple-icons` from 16.4.0 to 16.5.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.4.0...16.5.0)

---
updated-dependencies:
- dependency-name: ai-motion
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:27:23 +08:00
dependabot[bot] d7bfa42f85 chore(deps-dev): bump globals from 16.5.0 to 17.0.0 (#102)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:26:37 +08:00
Simon 551ba49280 Merge pull request #104 from alibaba/refactor/mv-brain-outof-llms
refactor: move agent logic out of `llms`
2026-01-13 14:24:29 +08:00
Simon adab24bb22 Merge branch 'refactor/mv-brain-outof-llms' of https://github.com/alibaba/page-agent into refactor/mv-brain-outof-llms 2026-01-13 14:22:48 +08:00
Simon 64aea7e84c chore: typing 2026-01-13 14:21:57 +08:00
Simon 67f6bd619e chore: rename autoFixer for better understanding 2026-01-13 14:20:52 +08:00
Simon 35d1fd1166 chore: better erroring
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:16:51 +08:00
Simon d5b8019fb1 chore: better docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:14:56 +08:00
Simon 526cb4dbb3 chore: rename OpenAIClient; clean up old notes 2026-01-13 13:58:21 +08:00
Simon 6dc56c57c6 chore: rm legacy llm client 2026-01-13 13:56:30 +08:00
Simon a3263fdd3a chore: rename OpenAICompatibleClient 2026-01-13 13:51:09 +08:00
Simon 14974c0257 feat!: mv brain from llms to agent; redo toolCall auto fixer 2026-01-13 13:49:19 +08:00
Simon e70ae40096 chore: guardrail 2026-01-12 13:55:39 +08:00
Simon 3e04a131f1 chore(version): bump version to 0.0.20 2026-01-11 01:40:54 +08:00
Simon b487a7900b feat(website): layout change 2026-01-11 01:40:21 +08:00
Simon 78b49c8b7a Merge pull request #99 from alibaba/refactor/website-structure
refactor: website structure
2026-01-11 01:21:55 +08:00
Simon 949e511dcf Merge pull request #98 from alibaba/feat/data-masking
feat: data masking
2026-01-11 01:17:08 +08:00
Simon d9faca5d01 refactor: normalize website structure 2026-01-11 01:16:57 +08:00
Simon 398c6d0869 chore: rm test-pages 2026-01-11 01:04:43 +08:00
Simon 54d478df1e docs: update data masking docs 2026-01-11 00:58:42 +08:00
Simon 43c80f5418 feat: transformPageContent hook for data-masking 2026-01-11 00:54:59 +08:00
Simon b186ffcaa6 chore: info about CDN 2026-01-11 00:05:21 +08:00
Simon 778bbda4b8 chore: remove roadmap 2026-01-10 23:53:19 +08:00
Simon f5900bd096 chore(version): bump version to 0.0.19 2026-01-10 23:35:02 +08:00
Simon 09772d79bd fix: panel language not found 2026-01-10 23:33:36 +08:00
Simon bfa6a5347e fix: rm maxTokens from api 2026-01-10 23:27:08 +08:00
Simon da5cf44012 Merge pull request #97 from alibaba/feat/instructions
feat: custom instructions
2026-01-10 19:27:27 +08:00
Simon 1666c08e58 refactor: rename instructions to custom-instructions 2026-01-10 19:25:56 +08:00
Simon 0c0c76fdd8 feat: wrap user callback in try catch
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-10 19:23:02 +08:00
Simon 6b0b1218ac chore: cleanup 2026-01-10 19:23:01 +08:00
Simon d70f652285 chore(website): add instructions for PageAgent demo page 2026-01-10 19:08:50 +08:00
Simon c3c01b63fb docs: upgrade docs for custom instructions 2026-01-10 19:02:18 +08:00
Simon 463d010c24 feat(agent): add instructions api 2026-01-10 18:44:09 +08:00
Simon 4e52f17b9f feat(agent): mv <agent_state> in front of <agent_history> 2026-01-10 17:44:08 +08:00
Simon 5237edfcca Merge branch 'main' of https://github.com/alibaba/page-agent 2026-01-07 19:58:56 +08:00
Simon 79dcf9d9e4 docs: fix api doc, should be model instead of modelName 2026-01-07 19:58:43 +08:00
Simon 09544205ed Merge pull request #91 from alibaba/dependabot/npm_and_yarn/production-dependencies-d50d67c463
chore(deps): bump the production-dependencies group across 1 directory with 3 updates
2026-01-07 19:28:54 +08:00
Simon 2c74c3170a Merge pull request #92 from alibaba/dependabot/npm_and_yarn/development-dependencies-f82d5f2ef0
chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
2026-01-07 19:28:40 +08:00
dependabot[bot] 9cee8f4174 chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
Bumps the development-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.2.0` | `20.3.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.2.0` | `20.3.0` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.0` | `6.0.1` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.3.13` | `2.5.1` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.3.13` | `2.5.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.50.1` | `8.52.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.0` | `16.5.1` |



Updates `@commitlint/cli` from 20.2.0 to 20.3.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.2.0 to 20.3.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.0/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.0...v6.0.1)

Updates `eslint-plugin-react-dom` from 2.3.13 to 2.5.1
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.1/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.3.13 to 2.5.1
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.1/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.50.1 to 8.52.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.52.0/packages/typescript-eslint)

Updates `react-i18next` from 16.5.0 to 16.5.1
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.0...v16.5.1)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.52.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:38:18 +00:00
dependabot[bot] d05b43cf50 chore(deps): bump the production-dependencies group across 1 directory with 3 updates
Bumps the production-dependencies group with 3 updates in the / directory: [zod](https://github.com/colinhacks/zod), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `zod` from 4.2.1 to 4.3.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.2.1...v4.3.5)

Updates `motion` from 12.23.26 to 12.24.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.23.26...v12.24.0)

Updates `simple-icons` from 16.2.0 to 16.4.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.2.0...16.4.0)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:37:33 +00:00
Simon 3fe6ae6862 chore(version): bump version to 0.0.18 2025-12-24 19:32:39 +08:00
Simon 66be1e56e3 fix: tool description lost 2025-12-24 19:28:59 +08:00
Simon 0fd4eaf9af chore: log network error for better debug 2025-12-24 19:06:35 +08:00
Simon a0712baa91 chore(version): bump version to 0.0.17 2025-12-24 19:02:57 +08:00
Simon 49af3a9c58 fix: illegal custom fetch 2025-12-24 19:00:43 +08:00
Simon 20ad3d04d2 docs(ROADMAP): check custom LLM fetch 2025-12-24 16:54:18 +08:00
131 changed files with 12919 additions and 15023 deletions
+1 -1
View File
@@ -58,4 +58,4 @@ body:
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
validations:
required: true
required: true
+1 -1
View File
@@ -44,4 +44,4 @@ body:
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true
validations:
required: true
required: true
+11 -3
View File
@@ -6,7 +6,7 @@ on:
- 'v*'
permissions:
id-token: write # Required for OIDC
id-token: write # Required for OIDC
contents: read
jobs:
@@ -24,7 +24,7 @@ jobs:
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
@@ -32,4 +32,12 @@ jobs:
run: npm run build:libs
- name: Publish all public packages
run: npm publish --workspaces --access public
run: |
VERSION=${GITHUB_REF#refs/tags/v}
if [[ "$VERSION" == *"-"* ]]; then
# Prerelease version (e.g., 0.3.0-beta.1) -> extract tag name before the dot
TAG=$(echo "$VERSION" | sed 's/.*-\([a-zA-Z]*\).*/\1/')
npm publish --workspaces --access public --tag "$TAG"
else
npm publish --workspaces --access public
fi
+7 -3
View File
@@ -25,11 +25,15 @@ dist-ssr
*.sw?
.qoder
#
# env files
.env
.env.*
# Copied files for npm publish (generated by prepublishOnly)
packages/page-agent/README.md
packages/page-agent/README-zh.md
packages/page-agent/LICENSE
packages/page-agent/LICENSE
# extension
.output
.wxt
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"]
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
+30 -26
View File
@@ -1,28 +1,32 @@
{
"editor.fontLigatures": true,
"cSpell.words": [
"deepseek",
"HITL",
"innerhtml",
"llms",
"opensource",
"qwen",
"retryable",
"shadcn",
"wouter"
],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false
}
"editor.fontLigatures": true,
"cSpell.words": [
"deepseek",
"historychange",
"HITL",
"innerhtml",
"llms",
"npmmirror",
"onwarn",
"opensource",
"qwen",
"retryable",
"shadcn",
"statuschange",
"wouter"
],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false
}
}
+36 -33
View File
@@ -4,14 +4,15 @@
This is a **monorepo** with npm workspaces:
- **Core Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages:
- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations, independent of LLM
- **UI** (`packages/ui/`) - Panel, SimulatorMask, i18n. Decoupled from PageAgent
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
## Development Commands
@@ -30,7 +31,8 @@ Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig
```
packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN
├── core/ # npm: "@page-agent/core" ⭐ Core agent logic (headless)
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms
├── page-controller/ # @page-agent/page-controller
@@ -41,10 +43,11 @@ packages/
### Module Boundaries
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui`
- **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`
- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel, Mask, i18n. No dependency on page-agent
- **Page Controller**: Pure DOM operations. No LLM or UI dependency
- **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface
- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
### PageController ↔ PageAgent Communication
@@ -69,47 +72,46 @@ const pageInfo = await this.pageController.getPageInfo()
3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index
### CDN Auto-Injection
Library auto-initializes via script tag:
```html
<script src="page-agent.js?model=gpt-4"></script>
```
Query params configure `PageAgentConfig` in `src/umd.ts`.
## Key Files Reference
### Page Agent (`packages/page-agent/`)
| File | Description |
|------|-------------|
| `src/PageAgent.ts` | ⭐ Main AI agent class |
| `src/umd.ts` | CDN/UMD entry with auto-init |
| `src/tools/` | Tool definitions calling PageController |
| File | Description |
| ------------------ | -------------------------------------------- |
| `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |
| `src/demo.ts` | IIFE demo entry (auto-init with demo API) |
### Core (`packages/core/`)
| File | Description |
| ---------------------- | --------------------------------------- |
| `src/PageAgentCore.ts` | ⭐ Core agent class without UI |
| `src/tools/` | Tool definitions calling PageController |
| `src/config/` | Configuration types and constants |
| `src/prompts/` | System prompt templates |
### LLMs (`packages/llms/`)
| File | Description |
|------|-------------|
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client |
| File | Description |
| --------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAIClient.ts` | OpenAI-compatible client |
### Page Controller (`packages/page-controller/`)
| File | Description |
|------|-------------|
| `src/PageController.ts` | ⭐ Main controller class |
| `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
| File | Description |
| --------------------------- | ---------------------------------------------------------- |
| `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
| `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features
### New Agent Tool
1. Implement in `packages/page-agent/src/tools/index.ts`
1. Implement in `packages/core/src/tools/index.ts`
2. If tool needs DOM ops, add method to PageController first
3. Tool calls `this.pageController.methodName()` for DOM interactions
@@ -123,4 +125,5 @@ Query params configure `PageAgentConfig` in `src/umd.ts`.
- Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English.
+94
View File
@@ -0,0 +1,94 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2026-01-19
### 🎉 First Stable Release
PageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.
### Features
#### Core
- **PageAgent** - Main entry class with built-in UI Panel
- **PageAgentCore** - Headless agent class for custom UI or programmatic use
- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration
- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs
- **Tool System** - Built-in tools for click, input, scroll, select, and more
- **Custom Tools** - Extend agent capabilities with your own tools (experimental)
- **Lifecycle Hooks** - Hook into agent execution (experimental)
- **Instructions System** - System-level and page-level instructions to guide agent behavior
- **Data Masking** - Transform page content before sending to LLM
#### Page Controller
- **Element Interactions** - Click, input text, select options, scroll
- **Visual Mask** - Blocks user interaction during automation
- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption
#### UI
- **Interactive Panel** - Real-time task progress and agent thinking display
- **Ask User Tool** - Agent can ask users for clarification
- **i18n Support** - English and Chinese localization
### Configuration
```typescript
interface PageAgentConfig {
// LLM Configuration (required)
baseURL: string
apiKey: string
model: string
temperature?: number
maxRetries?: number
customFetch?: typeof fetch
// Agent Configuration
language?: 'en-US' | 'zh-CN'
maxSteps?: number // default: 20
customTools?: Record<string, PageAgentTool> // experimental
instructions?: InstructionsConfig
transformPageContent?: (content: string) => string | Promise<string>
experimentalScriptExecutionTool?: boolean // default: false
// Lifecycle Hooks (experimental)
onBeforeTask?: (agent, result) => void
onAfterTask?: (agent, result) => void
onBeforeStep?: (agent, stepCount) => void
onAfterStep?: (agent, history) => void
onDispose?: (agent, reason?) => void
// Page Controller Configuration
enableMask?: boolean // default: true
viewportExpansion?: number
interactiveBlacklist?: Element[]
interactiveWhitelist?: Element[]
}
```
### Packages
| Package | Description |
| ----------------------------- | ---------------------------------- |
| `page-agent` | Main entry with UI Panel |
| `@page-agent/core` | Core agent logic without UI |
| `@page-agent/llms` | LLM client with retry logic |
| `@page-agent/page-controller` | DOM operations and visual feedback |
| `@page-agent/ui` | Panel and i18n |
### Known Limitations
- Single-page application only (cannot navigate across pages)
- No visual recognition (relies on DOM structure)
- Limited interaction support (no hover, drag-drop, canvas operations)
- See [Limitations](https://alibaba.github.io/page-agent/#/docs/introduction/limitations) for details
### Acknowledgments
This project builds upon the excellent work of [browser-use](https://github.com/browser-use/browser-use). DOM processing components and prompts are adapted from browser-use (MIT License).
+2 -2
View File
@@ -69,10 +69,10 @@ members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][https://www.contributor-covenant.org], version 1.4,
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
----
---
> Chinese Version
> 《阿里巴巴开源行为准则》
+65 -54
View File
@@ -7,29 +7,30 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Development Setup
1. **Prerequisites**
- `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well
- `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well
2. **Setup**
```bash
npm install
npm start # Start demo and documentation site
```
```bash
npm ci
npm start # Start demo and documentation site
```
### Project Structure
This is a **monorepo** with npm workspaces containing **two main packages**:
This is a **monorepo** with npm workspaces containing **3 main packages**:
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
- When developing. Use alias so that we don't have to pre-build.
- When bundling. Use external and disable ts `paths` alias to leave deps out.
- When bundling `UMD` and `Website`. Bundle everything including local packages.
- When bundling `IIFE` and `Website`. Bundle everything including local packages.
## 🤝 How to Contribute
@@ -44,40 +45,39 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
1. **Fork and Clone**
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
2. **Create Feature Branch**
```bash
git checkout -b feat/your-feature-name
```
```bash
git checkout -b feat/your-feature-name
```
3. **Make Changes**
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
4. **Test Your Changes**
```bash
# TODO
```
- Test in our demo website
- Test it on other websites if applicable
- `@TODO: test suite`
5. **Commit and Push**
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
6. **Create Pull Request**
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
## 📝 Code Style
@@ -94,41 +94,52 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
- BUT **NOT the core lib!!!** Be very careful if AI ever touched the core lib!!!
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
- Update the AI instructions when structure changed.
- Cursor and Cline: `./.cursor/rules`
- Github Copilot: `./.github/copilot-instructions.md`
- Claude Code: `./CLAUDE.md`
- Update the [AI instructions](AGENTS.md) when structure changed.
## 🔧 Development Workflows
### Test With Your Own LLM API
- Create a `.env` file in the repo root with your LLM API config
```env
LLM_MODEL_NAME=gpt-5.2
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api.your-llm-provider.com/v1
```
- Restart the dev server to load new env vars
- If not provided, the demo will the free testing proxy by default
### Website Development
```bash
npm start # React development server
npm start
```
### Core Lib Development and Testing
### Testing on Other Websites
> @TODO this part is outdated. Update this.
- Start and serve a local `iife` script
- Config your LLM API
- Start and serve a local umd script
```bash
npm run dev:demo # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.demo.js
```
```bash
npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs
```
- Add a new bookmark
- Add a new bookmark enable it on other website
```javascript
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.demo.js?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
```
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
- Click the bookmark on any page to load Page-Agent
> Warning: AK in your local `.env` will be inlined in the iife script.
### Adding Documentation
1. Create `website/src/docs/section/page-name/page.tsx`
2. Add route to `website/src/router.tsx`
3. Add navigation link to `website/src/components/DocsLayout.tsx`
Ask an AI to help you add documentation to the `website/` package. Follow the existing style.
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
## 🎯 Contribution Areas
@@ -143,7 +154,7 @@ We especially welcome contributions in:
## 🚫 What We Don't Accept
- Changes that break existing API compatibility
- Changes that break existing API compatibility (Discuss first)
- Heavy dependencies to core library
- Contributions without proper testing
- Code that doesn't follow project conventions
+2 -1
View File
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2025 Alibaba
Copyright (c) 2026 Alibaba
Copyright (c) 2026 Simon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+44 -46
View File
@@ -1,45 +1,58 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
🌐 [English](./README.md) | **中文**
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 轻松集成**
- 无需 Python,无需无头浏览器,无需浏览器插件。纯页面内脚本。
- **🔐 端侧运行**
- **🧠 HTML 脱水**
- **💬 自然语言接口**
- **🎨 HITL 交互界面**
## 🗺️ Roadmap
以及 😉
👉 [**Roadmap**](./ROADMAP.md)
- **🧪 实验性的 Chrome 扩展,支持跨页面控制** - `-b feat/ext`
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 快速开始
### CDN 集成
### 一行代码集成
通过我们免费的 Demo LLM 快速体验 PageAgent
```html
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
<script
src="https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js"
crossorigin="true"
></script>
```
> - **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,可能随时变更。
> - **🌷 建议使用自己的 LLM API。**
| 镜像 | URL |
| ------ | ----------------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js |
### NPM 安装
```bash
@@ -49,62 +62,43 @@ npm install page-agent
```javascript
import { PageAgent } from 'page-agent'
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN',
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
language: 'zh-CN',
})
await agent.execute('点击登录按钮')
```
适用于无法使用 NPM 的环境,我们也提供了 IIFE 构建的 CDN 方式。[@see CDN Usage](https://alibaba.github.io/page-agent/#/docs/integration/cdn-setup)
## 🏗️ 架构设计
PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
├── page-agent/ # Exported agent and demo(npm: page-agent)
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui)
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
└── website/ # 文档站点
```
## 🤝 贡献
欢迎社区贡献!以下是参与方式:
欢迎社区贡献!请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解环境配置和本地开发说明。
### 开发环境
1. Fork 项目仓库
2. Clone or fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. 安装依赖: `npm install`
4. 启动开发: `npm start`
### 贡献指南
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)和[贡献指南](CONTRIBUTING.md)。
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
## 👏 致谢
本项目基于以下优秀项目构建
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建
- **[browser-use](https://github.com/browser-use/browser-use)**
PageAgent 专为**客户端网页增强**设计,不是服务端自动化工具。
## 📄 许可证
MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
```
DOM processing components and prompt are derived from browser-use:
@@ -123,6 +117,10 @@ Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 许可证
[MIT License](LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
+43 -47
View File
@@ -1,45 +1,58 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i3/O1CN01MyVCS21EoKkIHUT1s_!!6000000000398-49-tps-1280-353.webp)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./README-zh.md)
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag.
- **🎯 Easy Integration**
- No python. No headless browser. No browser extension. Just in-page scripts.
- **🔐 Client-Side Processing**
- **🧠 DOM Extraction**
- **💬 Natural Language Interface**
- **🎨 UI with Human in the loop**
## 🗺️ Roadmap
And 😉
👉 [**Roadmap**](./ROADMAP.md)
- **🧪 `cross-page` control with an experimental chrome extension** - `-b feat/ext`
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 Quick Start
### CDN Integration
### One-line integration
Fastest way to try PageAgent with our free Demo LLM:
```html
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js"
crossorigin="true"
></script>
```
> - **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. May change without notice.
> - **🌷 Bring your own LLM API.**
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js |
### NPM Installation
```bash
@@ -49,18 +62,11 @@ npm install page-agent
```javascript
import { PageAgent } from 'page-agent'
// test server
// @note: rate limit. prompt limit. Origin limit. May change anytime. Use your own llm!
// @note Using official DeepSeek-chat(3.2). Go to DeepSeek website for privacy policy.
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'en-US',
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
language: 'en-US',
})
await agent.execute('Click the login button')
@@ -72,39 +78,25 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
├── page-agent/ # Exported agent and demo(npm: page-agent)
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui)
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
├── ui/ # Panel & i18n (npm: @page-agent/ui)
└── website/ # Demo & Documentation site
```
## 🤝 Contributing
We welcome contributions from the community! Here's how to get started:
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development.
### Setup
1. Fork the repository
2. Clone your fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. Install dependencies: `npm install`
4. Start development: `npm start`
### Contributing Guidelines
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) before contributing.
Please read [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
## 👏 Acknowledgments
This project builds upon the excellent work of:
This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
- **[browser-use](https://github.com/browser-use/browser-use)**
PageAgent is designed for **client-side web enhancement**, not server-side automation.
## 📄 License
MIT License - see the [LICENSE](LICENSE) file for details.
`PageAgent` is designed for **client-side web enhancement**, not server-side automation.
```
DOM processing components and prompt are derived from browser-use:
@@ -123,6 +115,10 @@ Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 License
[MIT License](LICENSE)
---
**⭐ Star this repo if you find PageAgent helpful!**
-61
View File
@@ -1,61 +0,0 @@
# 🗺️ PageAgent Roadmap
The development progress and future plans for PageAgent.
## 🚀 Current Works
- [x] **MVP**
- Core functionality implemented.
- [x] **SPA interaction**
- [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing**
- [x] **UI with HITL**
- Human-in-the-loop user interface. Agent can ask user questions.
- [x] **Landing and doc pages**
- [x] **Remove `ai-sdk`**
- Only one function of AI-ADK is being used.
- Our agent memory and thinking mechanism does not suite ai-sdk.
- [x] **Robust LLM output**
- Auto-fix incomplete output format of DeepSeek and QWen.
- [x] **Working homepage with live LLM API**
- [x] **~~free~~ CDN**
- [x] **Free evaluation plan**
- [x] **Custom actions and HITL**
- [ ] **Hooks and Events**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **User takeover**
- [#64](https://github.com/alibaba/page-agent/issues/64)
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [#45](https://github.com/alibaba/page-agent/issues/45)
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [#44](https://github.com/alibaba/page-agent/issues/44)
- [ ] **Improve Memory**
- [#66](https://github.com/alibaba/page-agent/issues/66)
- [#65](https://github.com/alibaba/page-agent/issues/65)
- [ ] **Optimize for popular UI frameworks**
- [x] **i18n of the website**
- [x] Chinese version
- [x] English version
- [x] **Refactor: Separate `Agent` and `PageController`**
- [ ] **Chrome-ext wrapper for multi-page tasks**
♻️ Following browser-use's update and contribute back.
## 📋 Pending Features
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [ ] **Support custom llm fetch**
- [ ] **Testing suits**
- [ ] **Same-origin multi-page-app rally**
- [ ] **Local MCP proxy**
## 🤔 To Be Decided
- [ ] **Cross-origin multi-page?**
- Tricky
- Need some kind of "memory rally"
+1 -6
View File
@@ -8,12 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig([
globalIgnores([
'**/dist',
'**/test-pages',
'**/node_modules',
'packages/website/src/components/ui',
]),
globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
{
plugins: {
'react-hooks': reactHooks,
+8207 -8229
View File
File diff suppressed because it is too large Load Diff
+121 -101
View File
@@ -1,103 +1,123 @@
{
"name": "root",
"private": true,
"version": "0.0.16",
"type": "module",
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/page-agent",
"packages/website"
],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"scripts": {
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:umd": "npm run dev:umd --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^20.2.0",
"@commitlint/config-conventional": "^20.2.0",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/node": "^25.0.3",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.3.13",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.3.13",
"globals": "^16.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.4",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.1",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.0",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.{jsx,tsx}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.css": [
"npx prettier --write --ignore-unknown"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"subject-case": [
0,
"never"
]
}
},
"prettier": {
"singleQuote": true,
"semi": false,
"useTabs": true,
"printWidth": 100,
"trailingComma": "es5",
"plugins": [
"@trivago/prettier-plugin-sort-imports"
],
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^(@/).*(?<!css)$",
"^[./].*(?<!css)$",
".css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
"name": "root",
"private": true,
"version": "1.0.0",
"type": "module",
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/core",
"packages/page-agent",
"packages/website"
],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"scripts": {
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:demo": "npm run dev:demo --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.9",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.7.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.7.2",
"globals": "^17.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.4",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"overrides": {
"typescript": "^5.9.3"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.{jsx,tsx}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.css": [
"npx prettier --write --ignore-unknown"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"subject-case": [
0,
"never"
]
}
},
"prettier": {
"singleQuote": true,
"semi": false,
"useTabs": true,
"printWidth": 100,
"trailingComma": "es5",
"plugins": [
"@trivago/prettier-plugin-sort-imports"
],
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^(@/).*(?<!css)$",
"^[./].*(?<!css)$",
".css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false,
"tabWidth": 4
}
},
{
"files": "*.json",
"options": {
"useTabs": false,
"tabWidth": 4
}
}
]
}
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.0.0",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "1.0.0",
"@page-agent/page-controller": "1.0.0"
}
}
+592
View File
@@ -0,0 +1,592 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
import type { PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import zod from 'zod'
import { type PageAgentConfig } from './config'
import { DEFAULT_MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import {
AgentActivity,
AgentReflection,
AgentStatus,
AgentStepEvent,
ExecutionResult,
HistoricalEvent,
MacroToolInput,
MacroToolResult,
} from './types'
import { normalizeResponse, trimLines, uid, waitFor } from './utils'
import { assert } from './utils/assert'
export { type PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
export type * from './types'
/**
* AI agent for browser DOM automation.
*
* @remarks
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `historychange` - History events updated (persistent, part of agent memory)
* - `activity` - Real-time activity feedback (transient, for UI only)
* - `dispose` - Agent cleanup triggered
*
* ## Information Streams
* 1. **History Events** (`history` array)
* - Persistent event stream that forms agent's memory
* - Included in LLM context across steps
* - Types: steps, observations, user takeovers, llm errors
*
* 2. **Activity Events** (via `activity` event)
* - Transient UI feedback during task execution
* - NOT included in LLM context
* - Types: thinking, executing, executed, retrying, error
*/
export class PageAgentCore extends EventTarget {
config: PageAgentConfig & { maxSteps: number }
id = uid()
tools: typeof tools
disposed = false
task = ''
taskId = ''
/** Agent execution status */
#status: AgentStatus = 'idle'
/**
* Callback for when agent needs user input (ask_user tool)
* If not set, ask_user tool will be disabled
* @example onAskUser: (q) => window.prompt(q) || ''
*/
onAskUser?: (question: string) => Promise<string>
#llm: LLM
#abortController = new AbortController()
/** PageController for DOM operations */
pageController: PageController
/** Runtime states for tracking across steps */
states = {
/** Accumulated wait time in seconds, used by wait tool */
totalWaitTime: 0,
/** Last known URL for detecting navigation */
lastURL: '',
}
/** History events */
history: HistoricalEvent[] = []
constructor(config: PageAgentConfig & { pageController: PageController }) {
super()
this.config = { ...config, maxSteps: config.maxSteps || DEFAULT_MAX_STEPS }
this.#llm = new LLM(this.config)
this.tools = new Map(tools)
this.pageController = config.pageController
// Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts } = (e as CustomEvent).detail
this.emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'retry',
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
attempt,
maxAttempts,
})
this.#emitHistoryChange()
})
this.#llm.addEventListener('error', (e) => {
const error = (e as CustomEvent).detail.error as Error | InvokeError
const message = String(error)
this.emitActivity({ type: 'error', message })
// Also push to history for panel rendering
this.history.push({
type: 'error',
message,
rawResponse: (error as InvokeError).rawResponse,
})
this.#emitHistoryChange()
})
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
}
/** Get current agent status */
get status(): AgentStatus {
return this.#status
}
/** Emit statuschange event */
#emitStatusChange(): void {
this.dispatchEvent(new Event('statuschange'))
}
/** Emit historychange event */
#emitHistoryChange(): void {
this.dispatchEvent(new Event('historychange'))
}
/**
* Emit activity event - for transient UI feedback
* @param activity - Current agent activity
*/
emitActivity(activity: AgentActivity): void {
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
}
/** Update status and emit event */
#setStatus(status: AgentStatus): void {
if (this.#status !== status) {
this.#status = status
this.#emitStatusChange()
}
}
/**
* Push a persistent observation to the history event stream.
* This will be visible in <agent_history> and remain in memory across steps.
*/
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
this.#emitHistoryChange()
}
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
// Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) {
this.tools.delete('ask_user')
}
const onBeforeStep = this.config.onBeforeStep
const onAfterStep = this.config.onAfterStep
const onBeforeTask = this.config.onBeforeTask
const onAfterTask = this.config.onAfterTask
await onBeforeTask?.(this)
// Show mask
await this.pageController.showMask()
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
this.#setStatus('running')
this.#emitHistoryChange()
// Reset states
this.states = {
totalWaitTime: 0,
lastURL: '',
}
try {
let step = 0
while (true) {
await this.#generateObservations(step)
await onBeforeStep?.(this, step)
console.group(`step: ${step}`)
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
// Thinking
console.log(chalk.blue('Thinking...'))
this.emitActivity({ type: 'thinking' })
const result = await this.#llm.invoke(
[
{
role: 'system',
content: this.#getSystemPrompt(),
},
{
role: 'user',
content: await this.#assembleUserPrompt(),
},
],
{ AgentOutput: this.#packMacroTool() },
this.#abortController.signal,
{
toolChoiceName: 'AgentOutput',
normalizeResponse,
}
)
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
const output = macroResult.output
const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory,
next_goal: input.next_goal,
}
const actionName = Object.keys(input.action)[0]
const action: AgentStepEvent['action'] = {
name: actionName,
input: input.action[actionName],
output: output,
}
this.history.push({
type: 'step',
stepIndex: step,
reflection,
action,
usage: result.usage,
rawResponse: result.rawResponse,
} as AgentStepEvent)
this.#emitHistoryChange()
console.log(chalk.green('Step finished:'), actionName)
console.groupEnd()
await onAfterStep?.(this, this.history)
step++
if (step > this.config.maxSteps) {
this.#onDone('Step count exceeded maximum limit', false)
const result: ExecutionResult = {
success: false,
data: 'Step count exceeded maximum limit',
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, text)
this.#onDone(text, success)
const result: ExecutionResult = {
success,
data: text,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
}
} catch (error: unknown) {
console.error('Task failed', error)
const errorMessage = String(error)
this.emitActivity({ type: 'error', message: errorMessage })
this.#onDone(errorMessage, false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
}
/**
* Merge all tools into a single MacroTool with the following input:
* - thinking: string
* - evaluation_previous_goal: string
* - memory: string
* - next_goal: string
* - action: { toolName: toolInput }
* where action must be selected from tools defined in this.tools
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
})
const actionSchema = zod.union(
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
)
const macroToolSchema = zod.object({
// thinking: zod.string().optional(),
evaluation_previous_goal: zod.string().optional(),
memory: zod.string().optional(),
next_goal: zod.string().optional(),
action: actionSchema,
})
return {
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('MacroTool execute'), input)
const action = input.action
const toolName = Object.keys(action)[0]
const toolInput = action[toolName]
// Build reflection text, only include non-empty fields
const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
}
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
// Emit executing activity
this.emitActivity({ type: 'executing', tool: toolName, input: toolInput })
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
// Emit executed activity
this.emitActivity({
type: 'executed',
tool: toolName,
input: toolInput,
output: result,
duration,
})
// Reset wait time for non-wait tools
if (toolName !== 'wait') {
this.states.totalWaitTime = 0
}
// Return structured result
return {
input,
output: result,
}
},
}
}
/**
* Get system prompt, dynamically replace language settings based on configured language
*/
#getSystemPrompt(): string {
let systemPrompt = SYSTEM_PROMPT
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
systemPrompt = systemPrompt.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
return systemPrompt
}
/**
* Get instructions from config and format as XML block
*/
async #getInstructions(): Promise<string> {
const { instructions } = this.config
if (!instructions) return ''
const systemInstructions = instructions.system?.trim()
const url = await this.pageController.getCurrentUrl()
let pageInstructions: string | undefined
if (instructions.getPageInstructions) {
try {
pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) {
console.error(
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
error
)
}
}
if (!systemInstructions && !pageInstructions) return ''
let result = '<instructions>\n'
if (systemInstructions) {
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
}
if (pageInstructions) {
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
}
result += '</instructions>\n\n'
return result
}
/**
* Generate observations before each step
* - URL change detection
* - Too many steps warning
* @todo loop detection
* @todo console error
*/
async #generateObservations(stepCount: number): Promise<void> {
// Detect URL change
const currentURL = await this.pageController.getCurrentUrl()
if (currentURL !== this.states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.states.lastURL = currentURL
await waitFor(0.5) // wait for page to stabilize
}
// Warn about remaining steps
const remaining = this.config.maxSteps - stepCount
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
}
async #assembleUserPrompt(): Promise<string> {
let prompt = ''
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
// - <user_request>
// - <step_info>
// <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += `<agent_state>
<user_request>
${this.task}
</user_request>
<step_info>
Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
Current date and time: ${new Date().toISOString()}
</step_info>
</agent_state>
`
// <agent_history>
// - <step_N> for steps
// - <sys> for observations and system messages
prompt += '\n<agent_history>\n'
let stepIndex = 0
for (const event of this.history) {
if (event.type === 'step') {
stepIndex++
prompt += `<step_${stepIndex}>
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
Memory: ${event.reflection.memory}
Next Goal: ${event.reflection.next_goal}
Action Results: ${event.action.output}
</step_${stepIndex}>
`
} else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
} else if (event.type === 'error') {
// Error events are mainly for panel rendering, not included in LLM context
// to avoid polluting the agent's reasoning with transient errors
}
}
prompt += '</agent_history>\n\n'
// <browser_state>
prompt += await this.#getBrowserState()
return trimLines(prompt)
}
#onDone(text: string, success = true) {
this.pageController.cleanUpHighlights()
this.pageController.hideMask() // No await - fire and forget
this.#setStatus(success ? 'completed' : 'error')
this.#abortController.abort()
}
async #getBrowserState(): Promise<string> {
const state = await this.pageController.getBrowserState()
let content = state.content
if (this.config.transformPageContent) {
content = await this.config.transformPageContent(content)
}
return trimLines(`<browser_state>
Current Page: [${state.title}](${state.url})
${state.header}
${content}
${state.footer}
</browser_state>
`)
}
dispose(reason?: string) {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')
// Emit dispose event for UI cleanup
this.dispatchEvent(new Event('dispose'))
this.config.onDispose?.(this, reason)
}
}
+1
View File
@@ -0,0 +1 @@
export const DEFAULT_MAX_STEPS = 20
+146
View File
@@ -0,0 +1,146 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller'
import type { PageAgentCore } from '../PageAgentCore'
import type { PageAgentTool } from '../tools'
import type { ExecutionResult, HistoricalEvent } from '../types'
export type { LLMConfig }
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig {
// theme?: 'light' | 'dark'
language?: SupportedLanguage
/**
* Maximum number of steps the agent can take per task.
* @default 20
*/
maxSteps?: number
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
/**
* Lifecycle hooks for task execution.
* @experimental API may change in future versions.
*
* All hooks receive the agent instance as first parameter.
*/
/**
* Called before each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param stepCount - Current step number (0-indexed)
*/
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
/**
* Called after each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param history - Current history of events
*/
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
/**
* Called before task execution starts.
* @experimental
* @param agent - The PageAgentCore instance
*/
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
/**
* Called after task execution completes (success or failure).
* @experimental
* @param agent - The PageAgentCore instance
* @param result - The execution result
*/
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* Called when the agent is disposed.
* @experimental
* @note This hook can block the disposal process if it's async.
* @param agent - The PageAgentCore instance
* @param reason - Optional reason for disposal
*/
onDispose?: (agent: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.md?raw' {
const content: string
export default content
}
@@ -0,0 +1 @@
system_prompt.md
@@ -92,7 +92,6 @@ Strictly follow these rules while using the browser and navigating the web:
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
- If you are not clear about the request or steps. `ask_user` to clarify it.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability>
@@ -120,7 +119,7 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
- `ask_user` for help if you have any difficulty. Users want to be kept in the loop.
- Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory.
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
</reasoning_rules>
@@ -4,7 +4,7 @@
*/
import zod, { type z } from 'zod'
import type { PageAgent } from '../PageAgent'
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
/**
@@ -14,7 +14,7 @@ export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
execute: (this: PageAgent, args: TParams) => Promise<string>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
}
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
@@ -36,7 +36,7 @@ tools.set(
text: zod.string(),
success: zod.boolean().default(true),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
// @note main loop will handle this one
// this.onDone(input.text, input.success)
return Promise.resolve('Task completed')
@@ -52,11 +52,20 @@ tools.set(
inputSchema: zod.object({
seconds: zod.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
this.states.totalWaitTime += input.seconds
if (this.states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
)
}
return `✅ Waited for ${input.seconds} seconds.`
},
})
@@ -70,9 +79,12 @@ tools.set(
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await this.panel.askUser(input.question)
return `✅ Received user answer: ${answer}`
execute: async function (this: PageAgentCore, input) {
if (!this.onAskUser) {
throw new Error('ask_user tool requires onAskUser callback to be set')
}
const answer = await this.onAskUser(input.question)
return `User answered: ${answer}`
},
})
)
@@ -84,7 +96,7 @@ tools.set(
inputSchema: zod.object({
index: zod.int().min(0),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.clickElement(input.index)
return result.message
},
@@ -99,7 +111,7 @@ tools.set(
index: zod.int().min(0),
text: zod.string(),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.inputText(input.index, input.text)
return result.message
},
@@ -115,7 +127,7 @@ tools.set(
index: zod.int().min(0),
text: zod.string(),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.selectOption(input.index, input.text)
return result.message
},
@@ -136,7 +148,7 @@ tools.set(
pixels: zod.number().int().min(0).optional(),
index: zod.number().int().min(0).optional(),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scroll({
...input,
numPages: input.num_pages,
@@ -156,7 +168,7 @@ tools.set(
pixels: zod.number().int().min(0),
index: zod.number().int().min(0).optional(),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scrollHorizontally(input)
return result.message
},
@@ -171,7 +183,7 @@ tools.set(
inputSchema: zod.object({
script: zod.string(),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.executeJavascript(input.script)
return result.message
},
+125
View File
@@ -0,0 +1,125 @@
/**
* Agent reflection state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentReflection {
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends Partial<AgentReflection> {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
/**
* A single agent step with reflection and action
*/
export interface AgentStepEvent {
type: 'step'
stepIndex: number
reflection: Partial<AgentReflection>
action: {
name: string
input: any
output: string
}
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
cachedTokens?: number
reasoningTokens?: number
}
/** Raw LLM response for debugging */
rawResponse?: unknown
}
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Retry event - LLM call is being retried
*/
export interface RetryEvent {
type: 'retry'
message: string
attempt: number
maxAttempts: number
}
/**
* Error event - fatal error from LLM or execution
*/
export interface AgentErrorEvent {
type: 'error'
message: string
rawResponse?: unknown
}
/**
* Union type for all history events
*/
export type HistoricalEvent =
| AgentStepEvent
| ObservationEvent
| UserTakeoverEvent
| RetryEvent
| AgentErrorEvent
/**
* Agent execution status
*/
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
export interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
+157
View File
@@ -0,0 +1,157 @@
import chalk from 'chalk'
/**
* Normalize LLM response and fix common format issues.
*
* Handles:
* - No tool_calls but JSON in message.content (fallback)
* - Model returns action name as tool call instead of AgentOutput
* - Arguments wrapped as double JSON string
* - Nested function call format
* - Missing action field (fallback to wait)
* - etc.
*/
export function normalizeResponse(response: any): any {
let resolvedArguments = null as any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
const message = choice.message
if (!message) throw new Error('No message in choice')
const toolCall = message.tool_calls?.[0]
// fix level and location of arguments
if (toolCall?.function?.arguments) {
resolvedArguments = safeJsonParse(toolCall.function.arguments)
// case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #1: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
// case: sometimes the model returns json in content instead of tool_calls
if (message.content) {
const content = message.content.trim()
const jsonInContent = retrieveJsonFromString(content)
if (jsonInContent) {
resolvedArguments = safeJsonParse(jsonInContent)
// case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #2: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
}
// case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') {
console.log(chalk.yellow(`[normalizeResponse] #3: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
}
// case: and sometimes action level only
// todo: needs better detection logic
if (
!resolvedArguments?.action &&
!resolvedArguments?.evaluation_previous_goal &&
!resolvedArguments?.memory &&
!resolvedArguments?.next_goal &&
!resolvedArguments?.thinking
) {
console.log(chalk.yellow(`[normalizeResponse] #4: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
throw new Error('No tool_call and the message content does not contain valid JSON')
}
} else {
throw new Error('No tool_call nor message content is present')
}
}
// fix double stringified arguments
resolvedArguments = safeJsonParse(resolvedArguments)
if (resolvedArguments.action) {
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
}
// fix incomplete formats
if (!resolvedArguments.action) {
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
}
// pack back to standard format
return {
...response,
choices: [
{
...choice,
message: {
...message,
tool_calls: [
{
...(toolCall || {}),
function: {
...(toolCall?.function || {}),
name: 'AgentOutput',
arguments: JSON.stringify(resolvedArguments),
},
},
],
},
},
],
}
}
/**
* Safely parse JSON, return original input if not json.
*/
function safeJsonParse(input: any): any {
if (typeof input === 'string') {
try {
return JSON.parse(input.trim())
} catch {
return input
}
}
return input
}
/**
* Extract and parse JSON from a string.
* - Treat content between the first `{` and the last `}` as JSON.
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
*/
function retrieveJsonFromString(str: string): any {
try {
const json = /({[\s\S]*})/.exec(str) ?? []
if (json.length === 0) {
return null
}
return JSON.parse(json[0]!)
} catch {
return null
}
}
interface Choice {
message?: {
role?: 'assistant'
content?: string
tool_calls?: {
id?: string
type?: 'function'
function?: {
name?: string
arguments?: string
}
}[]
}
index?: 0
finish_reason?: 'tool_calls'
}
@@ -1,3 +1,5 @@
export { normalizeResponse } from './autoFixer'
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
@@ -66,12 +68,13 @@ export function randomID(existingIDs?: string[]): string {
}
//
const _global = globalThis as any
if (!window.__PAGE_AGENT_IDS__) {
window.__PAGE_AGENT_IDS__ = []
if (!_global.__PAGE_AGENT_IDS__) {
_global.__PAGE_AGENT_IDS__ = []
}
const ids = window.__PAGE_AGENT_IDS__
const ids = _global.__PAGE_AGENT_IDS__
/**
* Generate a random ID.
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-check
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// ES Module for NPM Package
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
name: 'PageAgentCore',
fileName: 'page-agent-core',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: [
'chalk',
'zod',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
-41
View File
@@ -1,41 +0,0 @@
# @page-agent/llms
LLM client with a **reflection-before-action** mental model for page-agent.
## Why This Package Exists
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
## The Reflection-Before-Action Model
Every tool call must first output its reasoning state before the actual action:
```typescript
interface MacroToolInput {
// Reflection (mandatory before any action)
evaluation_previous_goal?: string // How well did the previous action work?
memory?: string // Key information to remember
next_goal?: string // What to accomplish next
// Action (the actual operation)
action: Record<string, any>
}
```
This design ensures that:
1. **The model evaluates its previous action** before deciding the next step
2. **Working memory is explicitly maintained** across conversation turns
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
## Key Components
| Export | Description |
|--------|-------------|
| `LLM` | Main LLM client class with retry logic |
| `MacroToolInput` | The reflection-before-action input schema |
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
| `LLMConfig` | Configuration for LLM connection |
| `parseLLMConfig` | Parse and apply defaults to config |
+41 -41
View File
@@ -1,43 +1,43 @@
{
"name": "@page-agent/llms",
"version": "0.0.16",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
}
},
"files": [
"dist/"
],
"description": "LLM client with reflection-before-action mental model for page-agent",
"keywords": [
"page-agent",
"llm",
"openai",
"tool-calling",
"agent"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/llms"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.2.1"
}
"name": "@page-agent/llms",
"version": "1.0.0",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
}
},
"files": [
"dist/"
],
"description": "LLM client with reflection-before-action mental model for page-agent",
"keywords": [
"page-agent",
"llm",
"openai",
"tool-calling",
"agent"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/llms"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.3.5"
}
}
+95 -67
View File
@@ -1,61 +1,58 @@
/**
* OpenAI Client implementation
* @note This client is only for demonstrating how to implement a LLM client.
* @note Use OpenAILenientClient instead.
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { modelPatch, zodToOpenAITool } from './utils'
/**
* @deprecated Use OpenAILenientClient instead.
* Client for OpenAI compatible APIs
*/
export class OpenAIClient implements LLMClient {
config: LLMConfig
config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: LLMConfig) {
constructor(config: Required<LLMConfig>) {
this.config = config
this.fetch = config.customFetch
}
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))
// Build request body
const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
}
// 2. Call API
let response: Response
try {
response = await fetch(`${this.config.baseURL}/chat/completions`, {
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
// model specific params
// reasoning_effort: 'minimal',
// verbosity: 'low',
parallel_tool_calls: false,
})
),
body: JSON.stringify(modelPatch(requestBody)),
signal: abortSignal,
})
} catch (error: unknown) {
// Network error
console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
}
@@ -93,91 +90,122 @@ export class OpenAIClient implements LLMClient {
)
}
// 4. Parse and validate response
const data = await response.json()
// 4. Check finish_reason
const choice = data.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
}
// Check finish_reason
switch (choice.finish_reason) {
case 'tool_calls':
// ✅ Normal
case 'function_call': // gemini
case 'stop': // some models use this even with tool calls
break
case 'length':
// ⚠️ Token limit reached
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached',
undefined,
data
)
case 'content_filter':
// ❌ Content filtered
throw new InvokeError(
InvokeErrorType.CONTENT_FILTER,
'Content filtered by safety system',
undefined,
data
)
case 'stop':
// ❌ Did not call tool (we require tool call)
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'Model did not call any tool', data)
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`,
undefined,
data
)
}
// 5. Parse tool call
const toolCall = choice.message?.tool_calls?.[0]
if (!toolCall) {
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'No tool call found in response', data)
}
// Apply normalizeResponse if provided (for fixing format issues automatically)
const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data
const normalizedChoice = (normalizedData as any).choices?.[0]
const toolName = toolCall.function.name
const tool = tools[toolName]
if (!tool) {
throw new InvokeError(InvokeErrorType.UNKNOWN, `Tool ${toolName} not found`, data)
}
// 6. Parse and validate arguments
let toolArgs: unknown
try {
toolArgs = JSON.parse(toolCall.function.arguments)
} catch (e) {
throw new InvokeError(InvokeErrorType.INVALID_TOOL_ARGS, 'Invalid JSON in tool arguments', e)
}
// Validate against zod schema
const validation = tool.inputSchema.safeParse(toolArgs)
if (!validation.success) {
// Get tool name from response
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
if (!toolCallName) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Tool arguments validation failed: ${validation.error.message}`,
validation.error
InvokeErrorType.NO_TOOL_CALL,
'No tool call found in response',
undefined,
data
)
}
// 7. Execute tool
const tool = tools[toolCallName]
if (!tool) {
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Tool "${toolCallName}" not found in tools`,
undefined,
data
)
}
// Extract and parse tool arguments
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
if (!argString) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'No tool call arguments found',
undefined,
data
)
}
let parsedArgs: unknown
try {
parsedArgs = JSON.parse(argString)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error,
data
)
}
// Validate with schema
const validation = tool.inputSchema.safeParse(parsedArgs)
if (!validation.success) {
console.error(validation.error)
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Tool arguments validation failed',
validation.error,
data
)
}
const toolInput = validation.data
// 5. Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(validation.data)
toolResult = await tool.execute(toolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
e,
data
)
}
// 8. Return result (including cache tokens)
// Return result
return {
toolCall: {
// id: toolCall.id,
name: toolName,
args: validation.data as Record<string, unknown>,
name: toolCallName,
args: toolInput,
},
toolResult,
usage: {
-124
View File
@@ -1,124 +0,0 @@
/**
* OpenAI Client implementation
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, MacroToolInput, Message, Tool } from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
this.config = config
this.fetch = config.customFetch
}
async invoke(
messages: Message[],
tools: { AgentOutput: Tool<MacroToolInput> },
abortSignal?: AbortSignal
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
// 2. Call API
let response: Response
try {
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
parallel_tool_calls: false,
})
),
signal: abortSignal,
})
} catch (error: unknown) {
// Network error
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
}
// 3. Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch()
const errorMessage =
(errorData as { error?: { message?: string } }).error?.message || response.statusText
if (response.status === 401 || response.status === 403) {
throw new InvokeError(
InvokeErrorType.AUTH_ERROR,
`Authentication failed: ${errorMessage}`,
errorData
)
}
if (response.status === 429) {
throw new InvokeError(
InvokeErrorType.RATE_LIMIT,
`Rate limit exceeded: ${errorMessage}`,
errorData
)
}
if (response.status >= 500) {
throw new InvokeError(
InvokeErrorType.SERVER_ERROR,
`Server error: ${errorMessage}`,
errorData
)
}
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`HTTP ${response.status}: ${errorMessage}`,
errorData
)
}
// parse response
const data = await response.json()
const tool = tools.AgentOutput
const macroToolInput = lenientParseMacroToolCall(data, tool.inputSchema as any)
// Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(macroToolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// Return result (including cache tokens)
return {
toolCall: {
// id: toolCall.id,
name: 'AgentOutput',
args: macroToolInput,
},
toolResult,
usage: {
promptTokens: data.usage?.prompt_tokens ?? 0,
completionTokens: data.usage?.completion_tokens ?? 0,
totalTokens: data.usage?.total_tokens ?? 0,
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
},
rawResponse: data,
}
}
}
+1 -19
View File
@@ -1,21 +1,3 @@
// Dev environment: use .env config if available, otherwise fallback to testing api
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
// Internal constants
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
export const DEFAULT_MAX_TOKENS = 16000
+5 -1
View File
@@ -25,14 +25,18 @@ export class InvokeError extends Error {
type: InvokeErrorType
retryable: boolean
statusCode?: number
/* raw error (provided if this error is caused by another error) */
rawError?: unknown
/* raw response from the API (provided if this error is caused by an API calling) */
rawResponse?: unknown
constructor(type: InvokeErrorType, message: string, rawError?: unknown) {
constructor(type: InvokeErrorType, message: string, rawError?: unknown, rawResponse?: unknown) {
super(message)
this.name = 'InvokeError'
this.type = type
this.retryable = this.isRetryable(type)
this.rawError = rawError
this.rawResponse = rawResponse
}
private isRetryable(type: InvokeErrorType): boolean {
+27 -77
View File
@@ -1,77 +1,26 @@
/**
* @topic LLM 与主流程的隔离
* @reasoning
* 将 llm 的调用和主流程分开是复杂的,
* 因为 agent 的 tool call 通常集成在 llm 模块中,而而先得到 llm 返回,然后处理工具调用
* tools 和 llm 调用的逻辑不可避免地耦合在一起,tool 的执行又和主流程耦合在一起
* 而 history 的维护和更新逻辑,又必须嵌入多轮 tool call 中
* @reasoning
* - 放弃框架提供的自动的多轮调用,每轮调用都由主流程发起
* - 理想情况下,llm 调用应该获得 structured output,然后由额外的模块触发 tool call,目前模型和框架都无法实现
* - 当前只能将 llm api 和 本地 tool call 耦合在一起,不关心其中的衔接方式
* @conclusion
* - @llm responsibility boundary:
* - call llm api with given messages and tools
* - invoke tool call and get the result of the tool
* - return the result to main loop
* - @main_loop responsibility boundary:
* - maintain all behaviors of an **agent**
* @conclusion
* - 这里的 llm 模块不是 agent,只负责一轮 llm 调用和工具调用,无状态
*/
/**
* @topic 结构化输出
* @facts
* - 几乎所有模型都支持 tool call schema
* - 几乎所有模型都支持返回 json
* - 只有 openAI/grok/gemini 支持 schema 并保证格式
* - 主流模型都支持 tool_choice: required
* - 除了 qwen 必须指定一个函数名 (9月上新后支持)
* @conclusion
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
*/
import { OpenAIClient } from './OpenAILenientClient'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
import { OpenAIClient } from './OpenAIClient'
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
import { InvokeError } from './errors'
import type {
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
} from './types'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
export type {
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
}
export type { InvokeError, InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.apiKey || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
'See: https://alibaba.github.io/page-agent/#/docs/features/models'
)
}
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
customFetch: config.customFetch ?? globalThis.fetch,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
@@ -95,20 +44,21 @@ export class LLM extends EventTarget {
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal: AbortSignal
abortSignal: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
return await withRetry(
async () => {
const result = await this.client.invoke(messages, tools, abortSignal)
const result = await this.client.invoke(messages, tools, abortSignal, options)
return result
},
// retry settings
{
maxRetries: this.config.maxRetries,
onRetry: (current: number) => {
onRetry: (attempt: number) => {
this.dispatchEvent(
new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } })
new CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } })
)
},
onError: (error: Error) => {
@@ -123,15 +73,15 @@ async function withRetry<T>(
fn: () => Promise<T>,
settings: {
maxRetries: number
onRetry: (retries: number) => void
onRetry: (attempt: number) => void
onError: (error: Error) => void
}
): Promise<T> {
let retries = 0
let attempt = 0
let lastError: Error | null = null
while (retries <= settings.maxRetries) {
if (retries > 0) {
settings.onRetry(retries)
while (attempt <= settings.maxRetries) {
if (attempt > 0) {
settings.onRetry(attempt)
await new Promise((resolve) => setTimeout(resolve, 100))
}
@@ -148,7 +98,7 @@ async function withRetry<T>(
if (error instanceof InvokeError && !error.retryable) throw error
lastError = error as Error
retries++
attempt++
await new Promise((resolve) => setTimeout(resolve, 100))
}
+23 -38
View File
@@ -32,6 +32,24 @@ export interface Tool<TParams = any, TResult = any> {
execute: (args: TParams) => Promise<TResult>
}
/**
* Invoke options for LLM call
*/
export interface InvokeOptions {
/**
* Force LLM to call a specific tool by name.
* If provided: tool_choice = { type: 'function', function: { name: toolChoiceName } }
* If not provided: tool_choice = 'required' (must call some tool, but model chooses which)
*/
toolChoiceName?: string
/**
* Response normalization function.
* Called before parsing the response.
* Used to fix various response format errors from the model.
*/
normalizeResponse?: (response: any) => any
}
/**
* LLM Client interface
* Note: Does not use generics because each tool in the tools array has different types
@@ -40,7 +58,8 @@ export interface LLMClient {
invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult>
}
@@ -68,12 +87,11 @@ export interface InvokeResult<TResult = unknown> {
* LLM configuration
*/
export interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
baseURL: string
apiKey: string
model: string
temperature?: number
maxTokens?: number
maxRetries?: number
/**
@@ -83,36 +101,3 @@ export interface LLMConfig {
*/
customFetch?: typeof globalThis.fetch
}
/**
* Agent brain state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentBrain {
// thinking?: string
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends AgentBrain {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
+18 -175
View File
@@ -4,8 +4,7 @@
import chalk from 'chalk'
import { z } from 'zod'
import { InvokeError, InvokeErrorType } from './errors'
import type { MacroToolInput, Tool } from './types'
import type { Tool } from './types'
function debug(message: string) {
console.debug(chalk.gray('[LLM]'), message)
@@ -26,176 +25,6 @@ export function zodToOpenAITool(name: string, tool: Tool) {
}
}
/**
* Although some models cannot guarantee correct response. Common issues are fixable:
* - Instead of returning a proper tool call. Return the tool call parameters in the message content.
* - Returned tool calls or messages don't follow the nested MacroToolInput format.
*/
export function lenientParseMacroToolCall(
responseData: any,
inputSchema: z.ZodObject<MacroToolInput & Record<string, any>>
): MacroToolInput {
// check
const choice = responseData.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', responseData)
}
// check
switch (choice.finish_reason) {
case 'tool_calls':
case 'function_call': // gemini
case 'stop': // will try a robust parse
// ✅ Normal
break
case 'length':
// ⚠️ Token limit reached
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached'
)
case 'content_filter':
// ❌ Content filtered
throw new InvokeError(InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system')
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`
)
}
// Extract action schema from MacroToolInput schema
const actionSchema = inputSchema.shape.action
if (!actionSchema) {
throw new Error('inputSchema must have an "action" field')
}
// patch stopReason mis-format
let arg: string | null = null
// try to use tool call
const toolCall = choice.message?.tool_calls?.[0]?.function
arg = toolCall?.arguments ?? null
if (arg && toolCall.name !== 'AgentOutput') {
// TODO: check if toolCall.name is a valid action name
// case: instead of AgentOutput, the model returned a action name as tool call
console.log(chalk.yellow('lenientParseMacroToolCall: #1 fixing incorrect tool call'))
let tmpArg
try {
tmpArg = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
arg = JSON.stringify({ action: { [toolCall.name]: tmpArg } })
}
if (!arg) {
// try to use message content as JSON
arg = choice.message?.content.trim() || null
}
if (!arg) {
throw new InvokeError(
InvokeErrorType.NO_TOOL_CALL,
'No tool call or content found in response',
responseData
)
}
// make sure is valid JSON
let parsedArgs: any
try {
parsedArgs = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
// patch incomplete formats
if (parsedArgs.action || parsedArgs.evaluation_previous_goal || parsedArgs.next_goal) {
// case: nested MacroToolInput format (correct format)
// some models may give a empty action (they may think reasoning and action should be separate)
if (!parsedArgs.action) {
console.log(chalk.yellow('lenientParseMacroToolCall: #2 fixing incorrect tool call'))
parsedArgs.action = {
wait: { seconds: 1 },
}
}
} else if (parsedArgs.type && parsedArgs.function) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.function.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.function.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #3 fixing incorrect tool call'))
parsedArgs = parsedArgs.function.arguments
} else if (parsedArgs.name && parsedArgs.arguments) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #4 fixing incorrect tool call'))
parsedArgs = parsedArgs.arguments
} else {
// case: only action parameters provided, wrap into MacroToolInput
// TODO: check if action name is valid
console.log(chalk.yellow('lenientParseMacroToolCall: #5 fixing incorrect tool call'))
parsedArgs = { action: parsedArgs } as MacroToolInput
}
// make sure it's not wrapped as string
if (typeof parsedArgs === 'string') {
console.log(chalk.yellow('lenientParseMacroToolCall: #6 fixing incorrect tool call'))
try {
parsedArgs = JSON.parse(parsedArgs)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse nested tool arguments as JSON',
error
)
}
}
const validation = inputSchema.safeParse(parsedArgs)
if (validation.success) {
return validation.data as unknown as MacroToolInput
} else {
const action = parsedArgs.action ?? {}
const actionName = Object.keys(action)[0] || 'unknown'
const actionArgs = JSON.stringify(action[actionName] || 'unknown')
// TODO: check if action name is valid. give a readable error message
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Tool arguments validation failed: action "${actionName}" with args ${actionArgs}`,
validation.error
)
}
}
/**
* Patch model specific parameters
*/
@@ -205,11 +34,25 @@ export function modelPatch(body: Record<string, any>) {
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: change tool_choice and disable thinking')
body.tool_choice = { type: 'tool', name: 'AgentOutput' }
debug('Applying Claude patch: disable thinking')
body.thinking = { type: 'disabled' }
// body.reasoning = { enabled: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
}
if (modelName.startsWith('grok')) {
+7 -8
View File
@@ -1,10 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+10 -11
View File
@@ -1,13 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
-1
View File
@@ -34,4 +34,3 @@ export default defineConfig({
'process.env.NODE_ENV': '"production"',
},
})
+52 -51
View File
@@ -1,53 +1,54 @@
{
"name": "page-agent",
"private": false,
"version": "0.0.16",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build && vite build --config vite.umd.config.js",
"serve": "npx serve dist/umd -p 5173",
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.2.1",
"@page-agent/llms": "0.0.16",
"@page-agent/page-controller": "0.0.16",
"@page-agent/ui": "0.0.16"
}
"name": "page-agent",
"private": false,
"version": "1.0.0",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build && npm run build:demo",
"build:demo": "vite build --config vite.iife.config.js",
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "1.0.0",
"@page-agent/page-controller": "1.0.0",
"@page-agent/core": "1.0.0",
"@page-agent/ui": "1.0.0"
}
}
+12 -485
View File
@@ -2,499 +2,26 @@
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import {
type AgentBrain,
LLM,
type MacroToolInput,
type MacroToolResult,
type Tool,
} from '@page-agent/llms'
import { type PageAgentConfig, PageAgentCore, type PageAgentTool, tool } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller'
import { Panel, SimulatorMask } from '@page-agent/ui'
import chalk from 'chalk'
import zod from 'zod'
import { Panel } from '@page-agent/ui'
import type { PageAgentConfig } from './config'
import { MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import { trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert'
export type { PageAgentConfig, PageAgentTool }
export { tool }
export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
export type { AgentBrain, MacroToolInput, MacroToolResult }
export interface AgentHistory {
brain: AgentBrain
action: {
name: string
input: any
output: string
}
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
cachedTokens?: number
reasoningTokens?: number
}
}
export interface ExecutionResult {
success: boolean
data: string
history: AgentHistory[]
}
export class PageAgent extends EventTarget {
config: PageAgentConfig
id = uid()
export class PageAgent extends PageAgentCore {
panel: Panel
tools: typeof tools
paused = false
disposed = false
task = ''
taskId = ''
#llm: LLM
#totalWaitTime = 0
#abortController = new AbortController()
#llmRetryListener: ((e: Event) => void) | null = null
#llmErrorListener: ((e: Event) => void) | null = null
#beforeUnloadListener: ((e: Event) => void) | null = null
/** PageController for DOM operations */
pageController: PageController
/** Fullscreen mask */
mask = new SimulatorMask()
/** History records */
history: AgentHistory[] = []
constructor(config: PageAgentConfig = {}) {
super()
this.config = config
this.#llm = new LLM(this.config)
this.panel = new Panel({
language: this.config.language,
onExecuteTask: (task) => this.execute(task),
onStop: () => this.dispose(),
onPauseToggle: () => {
this.paused = !this.paused
return this.paused
},
getPaused: () => this.paused,
})
this.tools = new Map(tools)
// Initialize PageController with config
this.pageController = new PageController(this.config)
// Listen to LLM events
this.#llmRetryListener = (e) => {
const { current, max } = (e as CustomEvent).detail
this.panel.update({ type: 'retry', current, max })
}
this.#llmErrorListener = (e) => {
const { error } = (e as CustomEvent).detail
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
}
this.#llm.addEventListener('retry', this.#llmRetryListener)
this.#llm.addEventListener('error', this.#llmErrorListener)
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
this.#beforeUnloadListener = (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
}
window.addEventListener('beforeunload', this.#beforeUnloadListener)
}
/**
* @todo maybe return something?
*/
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
const onBeforeStep = this.config.onBeforeStep || (() => void 0)
const onAfterStep = this.config.onAfterStep || (() => void 0)
const onBeforeTask = this.config.onBeforeTask || (() => void 0)
const onAfterTask = this.config.onAfterTask || (() => void 0)
await onBeforeTask.call(this)
// Show mask and panel
this.mask.show()
this.panel.show()
this.panel.reset()
this.panel.update({ type: 'input', task: this.task })
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
try {
let step = 0
while (true) {
await onBeforeStep.call(this, step)
console.group(`step: ${step}`)
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
// pause
await waitUntil(() => !this.paused)
// Update status to thinking
console.log(chalk.blue('Thinking...'))
this.panel.update({ type: 'thinking' })
const result = await this.#llm.invoke(
[
{
role: 'system',
content: this.#getSystemPrompt(),
},
{
role: 'user',
content: await this.#assembleUserPrompt(),
},
],
{ AgentOutput: this.#packMacroTool() },
this.#abortController.signal
)
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
const output = macroResult.output
const brain = {
evaluation_previous_goal: input.evaluation_previous_goal || '',
memory: input.memory || '',
next_goal: input.next_goal || '',
}
const actionName = Object.keys(input.action)[0]
const action = {
name: actionName,
input: input.action[actionName],
output: output,
}
this.history.push({
brain,
action,
usage: result.usage,
})
console.log(chalk.green('Step finished:'), actionName)
console.groupEnd()
await onAfterStep.call(this, step, this.history)
step++
if (step > MAX_STEPS) {
this.#onDone('Step count exceeded maximum limit', false)
const result: ExecutionResult = {
success: false,
data: 'Step count exceeded maximum limit',
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, text)
this.#onDone(text, success)
const result: ExecutionResult = {
success,
data: text,
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
}
} catch (error: unknown) {
console.error('Task failed', error)
this.#onDone(String(error), false)
const result: ExecutionResult = {
success: false,
data: String(error),
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
}
/**
* Merge all tools into a single MacroTool with the following input:
* - thinking: string
* - evaluation_previous_goal: string
* - memory: string
* - next_goal: string
* - action: { toolName: toolInput }
* where action must be selected from tools defined in this.tools
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return zod.object({
[toolName]: tool.inputSchema,
})
constructor(config: PageAgentConfig) {
const pageController = new PageController({
...config,
enableMask: config.enableMask ?? true,
})
const actionSchema = zod.union(
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
)
super({ ...config, pageController })
const macroToolSchema = zod.object({
// thinking: zod.string().optional(),
evaluation_previous_goal: zod.string().optional(),
memory: zod.string().optional(),
next_goal: zod.string().optional(),
action: actionSchema,
this.panel = new Panel(this, {
language: config.language,
})
return {
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
// pause
await waitUntil(() => !this.paused)
console.log(chalk.blue.bold('MacroTool execute'), input)
const action = input.action
const toolName = Object.keys(action)[0]
const toolInput = action[toolName]
const brain = trimLines(`✅: ${input.evaluation_previous_goal}
💾: ${input.memory}
🎯: ${input.next_goal}
`)
console.log(brain)
this.panel.update({ type: 'thinking', text: brain })
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
let result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
if (toolName === 'wait') {
this.#totalWaitTime += Math.round(toolInput.seconds + duration / 1000)
result += `\n<sys> You have waited ${this.#totalWaitTime} seconds accumulatively.`
if (this.#totalWaitTime >= 3)
result += '\nDo NOT wait any longer unless you have a good reason.\n'
result += '</sys>'
} else {
// For other tools, reset wait time
this.#totalWaitTime = 0
}
// Briefly display execution result
this.panel.update({
type: 'toolCompleted',
toolName,
args: toolInput,
result,
duration,
})
// Wait a moment to let user see the result
await new Promise((resolve) => setTimeout(resolve, 100))
// Return structured result
return {
input,
output: result,
}
},
}
}
/**
* Get system prompt, dynamically replace language settings based on configured language
*/
#getSystemPrompt(): string {
let systemPrompt = SYSTEM_PROMPT
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
systemPrompt = systemPrompt.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
return systemPrompt
}
async #assembleUserPrompt(): Promise<string> {
let prompt = ''
// <agent_history>
// - <step_>
prompt += '<agent_history>\n'
this.history.forEach((history, index) => {
prompt += `<step_${index + 1}>
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal}
Memory: ${history.brain.memory}
Next Goal: ${history.brain.next_goal}
Action Results: ${history.action.output}
</step_${index + 1}>
`
})
prompt += '</agent_history>\n\n'
// <agent_state>
// - <user_request>
// - <step_info>
// <agent_state>
prompt += `<agent_state>
<user_request>
${this.task}
</user_request>
<step_info>
Step ${this.history.length + 1} of ${MAX_STEPS} max possible steps
Current date and time: ${new Date().toISOString()}
</step_info>
</agent_state>
`
// <browser_state>
prompt += await this.#getBrowserState()
return trimLines(prompt)
}
#onDone(text: string, success = true) {
this.pageController.cleanUpHighlights()
// Update panel status
if (success) {
this.panel.update({ type: 'output', text })
} else {
this.panel.update({ type: 'error', message: text })
}
// Task completed
this.panel.update({ type: 'completed' })
this.mask.hide()
this.#abortController.abort()
}
async #getBrowserState(): Promise<string> {
const pageUrl = await this.pageController.getCurrentUrl()
const pageTitle = await this.pageController.getPageTitle()
const pi = await this.pageController.getPageInfo()
const viewportExpansion = await this.pageController.getViewportExpansion()
this.mask.wrapper.style.pointerEvents = 'none'
await this.pageController.updateTree()
this.mask.wrapper.style.pointerEvents = 'auto'
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
let prompt = trimLines(`<browser_state>
Current Page: [${pageTitle}](${pageUrl})
Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page
${viewportExpansion === -1 ? 'Interactive elements from top layer of the current page (full page):' : 'Interactive elements from top layer of the current page inside the viewport:'}
`)
// Page header info
const has_content_above = pi.pixels_above > 4
if (has_content_above && viewportExpansion !== -1) {
prompt += `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...\n`
} else {
prompt += `[Start of page]\n`
}
// Current viewport info
prompt += simplifiedHTML
prompt += `\n`
// Page footer info
const has_content_below = pi.pixels_below > 4
if (has_content_below && viewportExpansion !== -1) {
prompt += `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...\n`
} else {
prompt += `[End of page]\n`
}
prompt += `</browser_state>\n`
return prompt
}
dispose(reason?: string) {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
this.panel.dispose()
this.mask.dispose()
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')
// Clean up LLM event listeners
if (this.#llmRetryListener) {
this.#llm.removeEventListener('retry', this.#llmRetryListener)
this.#llmRetryListener = null
}
if (this.#llmErrorListener) {
this.#llm.removeEventListener('error', this.#llmErrorListener)
this.#llmErrorListener = null
}
// Clean up window event listeners
if (this.#beforeUnloadListener) {
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
this.#beforeUnloadListener = null
}
this.config.onDispose?.call(this, reason)
}
}
@@ -1,2 +0,0 @@
// Agent-specific constants (LLM constants moved to @page-agent/llms)
export const MAX_STEPS = 20
-84
View File
@@ -1,84 +0,0 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@page-agent/ui'
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
import type { PageAgentTool } from '../tools'
export type { LLMConfig }
export interface AgentConfig {
// theme?: 'light' | 'dark'
language?: SupportedLanguage
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see [tools](../tools/index.ts)
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
// lifecycle hooks
// @todo: use event instead of hooks
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/**
* @note this hook can block the disposal process
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
*/
onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
experimentalPreventNewPage?: boolean
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
@@ -1,5 +1,5 @@
/**
* Auto-run entry for page-agent.js. Insert this script into your page to get page-agent functionality.
* IIFE demo entry - auto-initializes with built-in demo API for testing
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
@@ -11,9 +11,6 @@ if (window.pageAgent) {
// Mount to global window object
window.PageAgent = PageAgent
// Export for ES module usage
// export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
@@ -21,9 +18,10 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
// in case document.x is not ready yet
// @todo give a switch to disable auto-init
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
let config: PageAgentConfig
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
@@ -31,14 +29,19 @@ setTimeout(() => {
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
config = { model, baseURL, apiKey, language }
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent()
config = {
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
}
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
// Create agent
window.pageAgent = new PageAgent(config)
window.pageAgent.panel.show()
window.pageAgent.panel.show() // Show panel
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
})
-11
View File
@@ -1,20 +1,9 @@
/// <reference types="vite/client" />
import type { PageAgent } from './PageAgent'
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.md?raw' {
const content: string
export default content
}
declare global {
interface Window {
pageAgent?: PageAgent
PageAgent: typeof PageAgent
__PAGE_AGENT_IDS__: string[]
}
}
-100
View File
@@ -1,100 +0,0 @@
/**
* Event mapping definitions
* @note Event bus callbacks must be repeatable without errors
*/
export interface PageAgentEventMap {
// PageAgent status events
// 'agent:execute': { params: { task: string } }
// 'agent:done': { params: { text: string; success: boolean } }
// 'agent:paused': { params: undefined }
// 'agent:resumed': { params: undefined }
// 'agent:disposed': { params: undefined }
// 'agent:error': { params: { error: string | Error } }
// Task status change events
'task:start': { params: { task: string } }
// 'task:complete': { params: { text: string; success: boolean } }
// 'task:error': { params: { error: string | Error } }
// Index signature for dynamic event names
// [key: string]: { params: any }
}
/**
* Event handler type definitions
*/
export type EventHandler<T extends keyof PageAgentEventMap> =
PageAgentEventMap[T]['params'] extends undefined
? () => void
: (params: PageAgentEventMap[T]['params']) => void
/**
* Async event handler type definitions
*/
export type AsyncEventHandler<T extends keyof PageAgentEventMap> =
PageAgentEventMap[T]['params'] extends undefined
? () => Promise<void>
: (params: PageAgentEventMap[T]['params']) => Promise<void>
/**
* Type-safe event bus
* @note Mainly used to decouple logic and UI
* @note All modules of a PageAgent instance share the same EventBus instance for communication
* @note Use with caution if delivery guarantee is needed for logic communication
* @note `on` `once` `emit` methods handle built-in events with type protection, use `addEventListener` for other events
*/
class EventBus extends EventTarget {
/**
* Listen to built-in events
*/
on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent
const params = customEvent.detail?.[0]
return handler(params)
}
this.addEventListener(event, wrappedHandler)
}
/**
* Listen to built-in events (one-time)
*/
once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent
const params = customEvent.detail?.[0]
return handler(params)
}
this.addEventListener(event, wrappedHandler, { once: true })
}
/**
* Emit built-in events
*/
emit<T extends keyof PageAgentEventMap>(
event: T,
...args: PageAgentEventMap[T]['params'] extends undefined
? []
: [PageAgentEventMap[T]['params']]
): void {
const customEvent = new CustomEvent(event, { detail: args })
this.dispatchEvent(customEvent)
return
}
}
const buses = new Map<string, EventBus>()
/**
* Get the event bus for a given channel
*/
export function getEventBus(channel: string) {
if (buses.has(channel)) {
return buses.get(channel)!
}
const bus = new EventBus()
buses.set(channel, bus)
return bus
}
export type { EventBus }
+7 -7
View File
@@ -1,9 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+24 -22
View File
@@ -1,24 +1,26 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../ui" }
]
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
}
@@ -1,4 +1,5 @@
// @ts-check
import { config as dotenvConfig } from 'dotenv'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
@@ -6,11 +7,14 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') })
// UMD Bundle for CDN
// - alias all local packages so that they can be build in
// - no external
// - no d.ts. dts does not work with monorepo aliasing
export default defineConfig({
export default defineConfig(({ mode }) => ({
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
@@ -20,26 +24,34 @@ export default defineConfig({
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/umd.ts'),
entry: resolve(__dirname, 'src/demo.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['umd'],
fileName: () => `page-agent.demo.js`,
formats: ['iife'],
},
outDir: resolve(__dirname, 'dist', 'umd'),
outDir: resolve(__dirname, 'dist', 'iife'),
cssCodeSplit: true,
// minify: false,
rollupOptions: {
output: {
// force use .js as extension
entryFileNames: 'page-agent.js',
// output: {
// // force use .js as extension
// entryFileNames: 'page-agent.js',
// },
onwarn: function (message, handler) {
if (message.code === 'EVAL') return
handler(message)
},
},
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
},
})
}))
+39 -36
View File
@@ -1,38 +1,41 @@
{
"name": "@page-agent/page-controller",
"version": "0.0.16",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
}
},
"files": [
"dist/"
],
"description": "Page controller for page-agent - DOM operations and element interactions",
"keywords": [
"page-agent",
"dom",
"browser-automation",
"web-automation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/page-controller"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}
"name": "@page-agent/page-controller",
"version": "1.0.0",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
}
},
"files": [
"dist/"
],
"description": "Page controller for page-agent - DOM operations and element interactions",
"keywords": [
"page-agent",
"dom",
"browser-automation",
"web-automation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/page-controller"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.8"
}
}
+124 -39
View File
@@ -25,6 +25,22 @@ import { patchReact } from './patches/react'
*/
export interface PageControllerConfig extends dom.DomConfig {
viewportExpansion?: number
/** Enable visual mask overlay during operations (default: false) */
enableMask?: boolean
}
/**
* Structured browser state for LLM consumption
*/
export interface BrowserState {
url: string
title: string
/** Page info + scroll position hint (e.g. "Page info: 1920x1080px...\n[Start of page]") */
header: string
/** Simplified HTML of interactive elements */
content: string
/** Page footer hint (e.g. "... 300 pixels below ..." or "[End of page]") */
footer: string
}
interface ActionResult {
@@ -64,14 +80,32 @@ export class PageController extends EventTarget {
/** last time the tree was updated */
private lastTimeUpdate = 0
/** Whether the tree has been indexed at least once */
private isIndexed = false
/** Visual mask overlay for blocking user interaction during automation */
private mask: InstanceType<typeof import('./mask/SimulatorMask').SimulatorMask> | null = null
private maskReady: Promise<void> | null = null
constructor(config: PageControllerConfig = {}) {
super()
this.config = config
patchReact(this)
if (config.enableMask) {
this.maskReady = this.initMask()
}
}
/**
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
*/
private async initMask(): Promise<void> {
const { SimulatorMask } = await import('./mask/SimulatorMask')
this.mask = new SimulatorMask()
}
// ======= State Queries =======
/**
@@ -81,42 +115,6 @@ export class PageController extends EventTarget {
return window.location.href
}
/**
* Get current page title
*/
async getPageTitle(): Promise<string> {
return document.title
}
/**
* Get page scroll and size info
*/
async getPageInfo() {
return getPageInfo()
}
/**
* Get the simplified HTML representation of the page.
* This is used by LLM to understand the page structure.
*/
async getSimplifiedHTML(): Promise<string> {
return this.simplifiedHTML
}
/**
* Get text description for an element by index
*/
async getElementText(index: number): Promise<string | undefined> {
return this.elementTextMap.get(index)
}
/**
* Get total number of indexed interactive elements
*/
async getElementCount(): Promise<number> {
return this.selectorMap.size
}
/**
* Get last tree update timestamp
*/
@@ -125,10 +123,43 @@ export class PageController extends EventTarget {
}
/**
* Get the viewport expansion setting
* Get structured browser state for LLM consumption.
* Automatically calls updateTree() to refresh the DOM state.
*/
async getViewportExpansion(): Promise<number> {
return this.config.viewportExpansion ?? VIEWPORT_EXPANSION
async getBrowserState(): Promise<BrowserState> {
const url = window.location.href
const title = document.title
const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
await this.updateTree()
const content = this.simplifiedHTML
// Build header: page info + scroll position hint
const pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`
const elementsLabel =
viewportExpansion === -1
? 'Interactive elements from top layer of the current page (full page):'
: 'Interactive elements from top layer of the current page inside the viewport:'
const hasContentAbove = pi.pixels_above > 4
const scrollHintAbove =
hasContentAbove && viewportExpansion !== -1
? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...`
: '[Start of page]'
const header = `${pageInfoLine}\n\n${elementsLabel}\n\n${scrollHintAbove}`
// Build footer: scroll position hint
const hasContentBelow = pi.pixels_below > 4
const footer =
hasContentBelow && viewportExpansion !== -1
? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...`
: '[End of page]'
return { url, title, header, content, footer }
}
// ======= DOM Tree Operations =======
@@ -136,12 +167,18 @@ export class PageController extends EventTarget {
/**
* Update DOM tree, returns simplified HTML for LLM.
* This is the main method to refresh the page state.
* Automatically bypasses mask during DOM extraction if enabled.
*/
async updateTree(): Promise<string> {
this.dispatchEvent(new Event('beforeUpdate'))
this.lastTimeUpdate = Date.now()
// Temporarily bypass mask to allow DOM extraction
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'none'
}
dom.cleanUpHighlights()
const blacklist = [
@@ -162,6 +199,14 @@ export class PageController extends EventTarget {
this.elementTextMap.clear()
this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)
// Mark as indexed - now element actions are allowed
this.isIndexed = true
// Restore mask blocking
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'auto'
}
this.dispatchEvent(new Event('afterUpdate'))
return this.simplifiedHTML
@@ -176,11 +221,22 @@ export class PageController extends EventTarget {
// ======= Element Actions =======
/**
* Ensure the tree has been indexed before any index-based operation.
* Throws if updateTree() hasn't been called yet.
*/
private assertIndexed(): void {
if (!this.isIndexed) {
throw new Error('DOM tree not indexed. Can not perform actions on elements.')
}
}
/**
* Click element by index
*/
async clickElement(index: number): Promise<ActionResult> {
try {
this.assertIndexed()
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await clickElement(element)
@@ -210,6 +266,7 @@ export class PageController extends EventTarget {
*/
async inputText(index: number, text: string): Promise<ActionResult> {
try {
this.assertIndexed()
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await inputTextElement(element, text)
@@ -231,6 +288,7 @@ export class PageController extends EventTarget {
*/
async selectOption(index: number, optionText: string): Promise<ActionResult> {
try {
this.assertIndexed()
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await selectOptionElement(element as HTMLSelectElement, optionText)
@@ -259,6 +317,8 @@ export class PageController extends EventTarget {
try {
const { down, numPages, pixels, index } = options
this.assertIndexed()
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
@@ -288,6 +348,8 @@ export class PageController extends EventTarget {
try {
const { right, pixels, index } = options
this.assertIndexed()
const scrollAmount = pixels * (right ? 1 : -1)
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
@@ -326,6 +388,26 @@ export class PageController extends EventTarget {
}
}
// ======= Mask Operations =======
/**
* Show the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
async showMask(): Promise<void> {
await this.maskReady
this.mask?.show()
}
/**
* Hide the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
async hideMask(): Promise<void> {
await this.maskReady
this.mask?.hide()
}
/**
* Dispose and clean up resources
*/
@@ -335,5 +417,8 @@ export class PageController extends EventTarget {
this.selectorMap.clear()
this.elementTextMap.clear()
this.simplifiedHTML = '<EMPTY>'
this.isIndexed = false
this.mask?.dispose()
this.mask = null
}
}
+87 -6
View File
@@ -287,12 +287,52 @@ export async function scrollVertically(
(document.documentElement as HTMLElement)
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
// Page-level scroll
const scrollBefore = window.scrollY
const scrollMax = document.documentElement.scrollHeight - window.innerHeight
window.scrollBy(0, dy)
return `✅ Scrolled page by ${dy}px.`
const scrollAfter = window.scrollY
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dy > 0
? `⚠️ Already at the bottom of the page, cannot scroll down further.`
: `⚠️ Already at the top of the page, cannot scroll up further.`
}
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`
if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`
return `✅ Scrolled page by ${scrolled}px.`
} else {
// Container scroll
const scrollBefore = el!.scrollTop
const scrollMax = el!.scrollHeight - el!.clientHeight
el!.scrollBy({ top: dy, behavior: 'smooth' })
await waitFor(0.1) // Animation playback
return `✅ Scrolled container (${el!.tagName}) by ${dy}px.`
await waitFor(0.1)
const scrollAfter = el!.scrollTop
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dy > 0
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
}
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
if (reachedTop)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
}
}
@@ -410,11 +450,52 @@ export async function scrollHorizontally(
(document.documentElement as HTMLElement)
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
// Page-level scroll
const scrollBefore = window.scrollX
const scrollMax = document.documentElement.scrollWidth - window.innerWidth
window.scrollBy(dx, 0)
return `✅ Scrolled page horizontally by ${dx}px`
const scrollAfter = window.scrollX
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dx > 0
? `⚠️ Already at the right edge of the page, cannot scroll right further.`
: `⚠️ Already at the left edge of the page, cannot scroll left further.`
}
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight)
return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`
if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`
return `✅ Scrolled page horizontally by ${scrolled}px.`
} else {
// Container scroll
const scrollBefore = el!.scrollLeft
const scrollMax = el!.scrollWidth - el!.clientWidth
el!.scrollBy({ left: dx, behavior: 'smooth' })
await waitFor(0.1) // Animation playback
return `✅ Scrolled container (${el!.tagName}) horizontally by ${dx}px`
await waitFor(0.1)
const scrollAfter = el!.scrollLeft
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dx > 0
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
}
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
if (reachedLeft)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
}
}
@@ -16,6 +16,7 @@
* @edit scrollable element detection
* @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements
*/
export default (
@@ -1446,7 +1447,14 @@ export default (
* @edit add `data-browser-use-ignore` attribute
*/
if (node.dataset?.browserUseIgnore === 'true') {
return true // Skip this node and its children
return null // Skip this node and its children
}
/**
* @edit exclude aria-hidden elements
*/
if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
return null // Skip this node and its children
}
// Special handling for root node (body)
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
+7 -7
View File
@@ -1,9 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+10 -10
View File
@@ -1,12 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
}
+5 -1
View File
@@ -29,7 +29,11 @@ export default defineConfig({
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['@page-agent/*'],
external: ['@page-agent/*', 'ai-motion'],
onwarn: function (message, handler) {
if (message.code === 'EVAL') return
handler(message)
},
},
minify: false,
sourcemap: true,
+36 -39
View File
@@ -1,41 +1,38 @@
{
"name": "@page-agent/ui",
"version": "0.0.16",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-ui.js",
"default": "./dist/lib/page-agent-ui.js"
}
},
"files": [
"dist/"
],
"description": "UI components for page-agent - Panel, SimulatorMask, and i18n",
"keywords": [
"page-agent",
"ui",
"panel",
"i18n"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/ui"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7"
}
"name": "@page-agent/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-ui.js",
"default": "./dist/lib/page-agent-ui.js"
}
},
"files": [
"dist/"
],
"description": "UI components for page-agent - Panel and i18n",
"keywords": [
"page-agent",
"ui",
"panel",
"i18n"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/ui"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}
}
-93
View File
@@ -1,93 +0,0 @@
/**
* Agent execution state management
*/
export interface Step {
id: string
stepNumber: number
timestamp: Date
type: 'thinking' | 'tool_executing' | 'completed' | 'error' | 'output' | 'input' | 'retry'
// Tool execution related
toolName?: string
toolArgs?: any
toolResult?: any
// Display data
displayText: string
duration?: number
}
export type AgentStatus = 'idle' | 'running' | 'paused' | 'completed' | 'error'
export class UIState {
private steps: Step[] = []
private currentStep: Step | null = null
private status: AgentStatus = 'idle'
private stepCounter = 0
addStep(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): Step {
const step: Step = {
id: this.generateId(),
stepNumber: ++this.stepCounter,
timestamp: new Date(),
...stepData,
}
this.steps.push(step)
this.currentStep = step
// Update overall status
this.updateStatus(step.type)
return step
}
updateCurrentStep(updates: Partial<Step>): Step | null {
if (!this.currentStep) return null
Object.assign(this.currentStep, updates)
return this.currentStep
}
getCurrentStep(): Step | null {
return this.currentStep
}
getAllSteps(): Step[] {
return [...this.steps]
}
getStatus(): AgentStatus {
return this.status
}
reset(): void {
this.steps = []
this.currentStep = null
this.status = 'idle'
this.stepCounter = 0
}
private updateStatus(stepType: Step['type']): void {
switch (stepType) {
case 'thinking':
case 'tool_executing':
case 'output':
case 'input':
case 'retry':
this.status = 'running'
break
case 'completed':
this.status = 'completed'
break
case 'error':
this.status = 'error'
break
}
}
private generateId(): string {
return `step_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class I18n {
constructor(language: SupportedLanguage = 'en-US') {
this.language = language in locales ? language : 'en-US'
this.translations = locales[language]
this.translations = locales[this.language]
}
// 类型安全的翻译方法
+2 -8
View File
@@ -4,17 +4,13 @@ const enUS = {
panel: {
ready: 'Ready',
thinking: 'Thinking...',
paused: 'Paused',
taskInput: 'Enter new task, describe steps in detail, press Enter to submit',
userAnswerPrompt: 'Please answer the question above, press Enter to submit',
taskTerminated: 'Task terminated',
taskCompleted: 'Task completed',
continueExecution: 'Continue execution',
userAnswer: 'User answer: {{input}}',
question: 'Question: {{question}}',
waitingPlaceholder: 'Waiting for task to start...',
pause: 'Pause',
continue: 'Continue',
stop: 'Stop',
expand: 'Expand history',
collapse: 'Collapse history',
@@ -26,6 +22,7 @@ const enUS = {
selecting: 'Selecting option "{{text}}"...',
scrolling: 'Scrolling page...',
waiting: 'Waiting {{seconds}} seconds...',
askingUser: 'Asking user...',
done: 'Task done',
clicked: '🖱️ Clicked element [{{index}}]',
inputted: '⌨️ Inputted text "{{text}}"',
@@ -54,17 +51,13 @@ const zhCN = {
panel: {
ready: '准备就绪',
thinking: '正在思考...',
paused: '暂停中,稍后',
taskInput: '输入新任务,详细描述步骤,回车提交',
userAnswerPrompt: '请回答上面问题,回车提交',
taskTerminated: '任务已终止',
taskCompleted: '任务结束',
continueExecution: '继续执行',
userAnswer: '用户回答: {{input}}',
question: '询问: {{question}}',
waitingPlaceholder: '等待任务开始...',
pause: '暂停',
continue: '继续',
stop: '终止',
expand: '展开历史',
collapse: '收起历史',
@@ -76,6 +69,7 @@ const zhCN = {
selecting: '正在选择选项 "{{text}}"...',
scrolling: '正在滚动页面...',
waiting: '等待 {{seconds}} 秒...',
askingUser: '正在询问用户...',
done: '结束任务',
clicked: '🖱️ 已点击元素 [{{index}}]',
inputted: '⌨️ 已输入文本 "{{text}}"',
+2 -3
View File
@@ -1,4 +1,3 @@
export { Panel, type PanelConfig, type PanelUpdate } from './Panel'
export { SimulatorMask } from './SimulatorMask'
export { UIState, type Step, type AgentStatus } from './UIState'
export { Panel, type PanelConfig } from './panel/Panel'
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
+1 -1
View File
@@ -2,4 +2,4 @@ This is the CSS implementation of ai-motion.
Easy to use but Terrible performance. Causing full screen glitching in some browsers.
Use it only in a small area.
Use it only in a small area.
@@ -209,18 +209,6 @@
}
}
.pauseButton {
font-weight: 600;
&.paused {
background: rgba(34, 197, 94, 0.2); /* 绿色背景表示可以继续 */
color: rgb(34, 197, 94);
&:hover {
background: rgba(34, 197, 94, 0.3);
}
}
}
.stopButton {
background: rgba(239, 68, 68, 0.2);
color: rgb(255, 41, 41);
@@ -364,6 +352,16 @@
background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));
}
&.observation {
border-left-color: rgb(147, 51, 234);
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));
}
&.question {
border-left-color: rgb(255, 159, 67);
background: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));
}
/* 突出显示 done 成功结果 */
&.doneSuccess {
background: linear-gradient(
@@ -446,7 +444,7 @@
.historyContent {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
word-break: break-all;
@@ -460,6 +458,12 @@
line-height: 1;
transition: all 0.3s ease;
}
.reflectionLines {
display: flex;
flex-direction: column;
gap: 4px;
}
}
.historyMeta {
@@ -1,6 +1,7 @@
import { type Step, UIState } from './UIState'
import { I18n, type SupportedLanguage } from './i18n'
import { truncate } from './utils'
import { I18n, type SupportedLanguage } from '../i18n'
import { truncate } from '../utils'
import { createCard, createReflectionLines, formatTime } from './cards'
import type { AgentActivity, PanelAgentAdapter } from './types'
import styles from './Panel.module.css'
@@ -9,29 +10,22 @@ import styles from './Panel.module.css'
*/
export interface PanelConfig {
language?: SupportedLanguage
onExecuteTask: (task: string) => void
onStop: () => void
onPauseToggle: () => boolean // returns new paused state
getPaused: () => boolean
/**
* Whether to prompt for next task after task completion
* @default true
*/
promptForNextTask?: boolean
}
/**
* Semantic update types - Panel handles i18n internally
*/
export type PanelUpdate =
| { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text
| { type: 'input'; task: string }
| { type: 'question'; question: string }
| { type: 'userAnswer'; input: string }
| { type: 'retry'; current: number; max: number }
| { type: 'error'; message: string }
| { type: 'output'; text: string }
| { type: 'completed' }
| { type: 'toolExecuting'; toolName: string; args: any }
| { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number }
/**
* Agent control panel
*
* Architecture:
* - History list: renders directly from agent.history (historical events)
* - Header bar: shows activity events (transient state) and agent status
*
* This separation ensures data consistency - history is the single source of truth
* for what has been done, while activity shows what is happening now.
*/
export class Panel {
#wrapper: HTMLElement
@@ -39,14 +33,13 @@ export class Panel {
#statusText: HTMLElement
#historySection: HTMLElement
#expandButton: HTMLElement
#pauseButton: HTMLElement
#stopButton: HTMLElement
#inputSection: HTMLElement
#taskInput: HTMLInputElement
#state = new UIState()
#isExpanded = false
#agent: PanelAgentAdapter
#config: PanelConfig
#isExpanded = false
#i18n: I18n
#userAnswerResolver: ((input: string) => void) | null = null
#isWaitingForUserAnswer: boolean = false
@@ -54,47 +47,145 @@ export class Panel {
#pendingHeaderText: string | null = null
#isAnimating = false
// Event handlers (bound for removal)
#onStatusChange = () => this.#handleStatusChange()
#onHistoryChange = () => this.#handleHistoryChange()
#onActivity = (e: Event) => this.#handleActivity((e as CustomEvent<AgentActivity>).detail)
#onAgentDispose = () => this.dispose()
get wrapper(): HTMLElement {
return this.#wrapper
}
constructor(config: PanelConfig) {
/**
* Create a Panel bound to an agent
* @param agent - Agent instance that implements PanelAgentAdapter
* @param config - Optional panel configuration
*/
constructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {
this.#agent = agent
this.#config = config
this.#i18n = new I18n(config.language ?? 'en-US')
// Set up askUser callback on agent
this.#agent.onAskUser = (question) => this.#askUser(question)
// Create UI elements
this.#wrapper = this.#createWrapper()
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
this.#pauseButton = this.#wrapper.querySelector(`.${styles.pauseButton}`)!
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
// Listen to agent events
this.#agent.addEventListener('statuschange', this.#onStatusChange)
this.#agent.addEventListener('historychange', this.#onHistoryChange)
this.#agent.addEventListener('activity', this.#onActivity)
this.#agent.addEventListener('dispose', this.#onAgentDispose)
this.#setupEventListeners()
this.#startHeaderUpdateLoop()
this.#showInputArea()
this.hide() // Start hidden
}
// ========== Agent event handlers ==========
/** Handle agent status change */
#handleStatusChange(): void {
const status = this.#agent.status
// Map agent status to UI indicator type
const indicatorType =
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
this.#updateStatusIndicator(indicatorType)
// Show/hide based on status
if (status === 'running') {
this.show()
this.#hideInputArea() // Hide input while running
}
// Handle completion
if (status === 'completed' || status === 'error') {
if (!this.#isExpanded) {
this.#expand()
}
if (this.#shouldShowInputArea()) {
this.#showInputArea()
}
}
}
/** Handle agent history change - re-render history list from agent.history */
#handleHistoryChange(): void {
this.#renderHistory()
}
/**
* Ask for user input
* Handle agent activity - transient state for immediate UI feedback
* Activity events are NOT persisted in history, only used for header bar updates
*/
async askUser(question: string): Promise<string> {
#handleActivity(activity: AgentActivity): void {
switch (activity.type) {
case 'thinking':
this.#pendingHeaderText = this.#i18n.t('ui.panel.thinking')
this.#updateStatusIndicator('thinking')
break
case 'executing':
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input)
this.#updateStatusIndicator('executing')
break
case 'executed':
this.#pendingHeaderText = truncate(activity.output, 50)
break
case 'retrying':
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`
this.#updateStatusIndicator('retrying')
break
case 'error':
this.#pendingHeaderText = truncate(activity.message, 50)
this.#updateStatusIndicator('error')
break
}
}
/**
* Ask for user input (internal, called by agent via onAskUser)
*/
#askUser(question: string): Promise<string> {
return new Promise((resolve) => {
// Set `waiting for user answer` state
this.#isWaitingForUserAnswer = true
this.#userAnswerResolver = resolve
// Update state to `running`
this.#updateInternal({
type: 'output',
displayText: this.#i18n.t('ui.panel.question', { question }),
}) // Expand history panel
// Expand history panel
if (!this.#isExpanded) {
this.#expand()
}
// Add temporary question card so user can see the full question
const tempCard = document.createElement('div')
tempCard.innerHTML = createCard({
icon: '❓',
content: `Question: ${question}`,
meta: formatTime(this.#config.language ?? 'en-US'),
type: 'question',
})
const cardElement = tempCard.firstElementChild as HTMLElement
cardElement.setAttribute('data-temp-card', 'true')
this.#historySection.appendChild(cardElement)
this.#scrollToBottom()
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
})
}
@@ -115,16 +206,10 @@ export class Panel {
}
reset(): void {
this.#state.reset()
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#updateHistory()
this.#renderHistory()
this.#collapse()
// Reset pause state via callback
if (this.#config.getPaused()) {
this.#config.onPauseToggle()
}
this.#updatePauseButton()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
@@ -141,17 +226,16 @@ export class Panel {
}
/**
* Update panel with semantic data - i18n handled internally
*/
update(data: PanelUpdate): void {
const stepData = this.#toStepData(data)
this.#updateInternal(stepData)
}
/**
* Dispose panel
* Dispose panel and clean up event listeners
*/
dispose(): void {
// Remove agent event listeners
this.#agent.removeEventListener('statuschange', this.#onStatusChange)
this.#agent.removeEventListener('historychange', this.#onHistoryChange)
this.#agent.removeEventListener('activity', this.#onActivity)
this.#agent.removeEventListener('dispose', this.#onAgentDispose)
// Clean up UI
this.#isWaitingForUserAnswer = false
this.#stopHeaderUpdateLoop()
this.wrapper.remove()
@@ -159,67 +243,21 @@ export class Panel {
// ========== Private methods ==========
/**
* Convert semantic update to step data with i18n
*/
#toStepData(data: PanelUpdate): Omit<Step, 'id' | 'stepNumber' | 'timestamp'> {
switch (data.type) {
case 'thinking':
return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') }
case 'input':
return { type: 'input', displayText: data.task }
case 'question':
return {
type: 'output',
displayText: this.#i18n.t('ui.panel.question', { question: data.question }),
}
case 'userAnswer':
return {
type: 'input',
displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }),
}
case 'retry':
return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` }
case 'error':
return { type: 'error', displayText: data.message }
case 'output':
return { type: 'output', displayText: data.text }
case 'completed':
return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') }
case 'toolExecuting':
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
displayText: this.#getToolExecutingText(data.toolName, data.args),
}
case 'toolCompleted': {
const displayText = this.#getToolCompletedText(data.toolName, data.args)
if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
toolResult: data.result,
displayText,
duration: data.duration,
}
}
}
}
#getToolExecutingText(toolName: string, args: any): string {
#getToolExecutingText(toolName: string, args: unknown): string {
const a = args as Record<string, string | number>
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicking', { index: args.index })
return this.#i18n.t('ui.tools.clicking', { index: a.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputting', { index: args.index })
return this.#i18n.t('ui.tools.inputting', { index: a.index })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selecting', { text: args.text })
return this.#i18n.t('ui.tools.selecting', { text: a.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolling')
case 'wait':
return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds })
return this.#i18n.t('ui.tools.waiting', { seconds: a.seconds })
case 'ask_user':
return this.#i18n.t('ui.tools.askingUser')
case 'done':
return this.#i18n.t('ui.tools.done')
default:
@@ -227,100 +265,11 @@ export class Panel {
}
}
#getToolCompletedText(toolName: string, args: any): string | null {
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicked', { index: args.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputted', { text: args.text })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selected', { text: args.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolled')
case 'wait':
return this.#i18n.t('ui.tools.waited')
case 'done':
return null
default:
return null
}
}
/**
* Update status (internal)
*/
#updateInternal(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
// Skip empty displayText (filtered toolCompleted for 'done')
if (!stepData.displayText) return
const step = this.#state.addStep(stepData)
// Queue header text update (will be processed by periodic check)
const headerText = truncate(step.displayText, 20)
this.#pendingHeaderText = headerText
this.#updateStatusIndicator(step.type)
this.#updateHistory()
// Auto-expand history after task completion
if (step.type === 'completed' || step.type === 'error') {
if (!this.#isExpanded) {
this.#expand()
}
}
// Control input area display based on status
if (this.#shouldShowInputArea()) {
this.#showInputArea()
} else {
this.#hideInputArea()
}
}
/**
* Toggle pause state
*/
#togglePause(): void {
const paused = this.#config.onPauseToggle()
this.#updatePauseButton()
// Update status display
if (paused) {
this.#statusText.textContent = this.#i18n.t('ui.panel.paused')
this.#updateStatusIndicator('thinking')
} else {
this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution')
this.#updateStatusIndicator('tool_executing')
}
}
/**
* Update pause button state
*/
#updatePauseButton(): void {
const paused = this.#config.getPaused()
if (paused) {
this.#pauseButton.textContent = '▶'
this.#pauseButton.title = this.#i18n.t('ui.panel.continue')
this.#pauseButton.classList.add(styles.paused)
} else {
this.#pauseButton.textContent = '⏸︎'
this.#pauseButton.title = this.#i18n.t('ui.panel.pause')
this.#pauseButton.classList.remove(styles.paused)
}
}
/**
* Stop Agent
*/
#stopAgent(): void {
// Update status display
this.#updateInternal({
type: 'error',
displayText: this.#i18n.t('ui.panel.taskTerminated'),
})
this.#config.onStop()
this.#agent.dispose()
}
/**
@@ -337,7 +286,8 @@ export class Panel {
// Handle user input mode
this.#handleUserAnswer(input)
} else {
this.#config.onExecuteTask(input)
// Execute task via agent
this.#agent.execute(input)
}
}
@@ -345,10 +295,11 @@ export class Panel {
* Handle user answer
*/
#handleUserAnswer(input: string): void {
// Add user input to history
this.#updateInternal({
type: 'input',
displayText: this.#i18n.t('ui.panel.userAnswer', { input }),
// Remove temporary question cards (only direct children for safety)
Array.from(this.#historySection.children).forEach((child) => {
if (child.getAttribute('data-temp-card') === 'true') {
child.remove()
}
})
// Reset state
@@ -389,32 +340,38 @@ export class Panel {
// Always show input area if waiting for user input
if (this.#isWaitingForUserAnswer) return true
const steps = this.#state.getAllSteps()
if (steps.length === 0) {
const history = this.#agent.history
if (history.length === 0) {
return true // Initial state
}
const lastStep = steps[steps.length - 1]
return lastStep.type === 'completed' || lastStep.type === 'error'
const status = this.#agent.status
const isTaskEnded = status === 'completed' || status === 'error'
// Only show input area after task completion if configured to do so
if (isTaskEnded) {
return this.#config.promptForNextTask ?? true
}
return false
}
#createWrapper(): HTMLElement {
const wrapper = document.createElement('div')
wrapper.id = 'page-agent-runtime_agent-panel'
wrapper.className = `${styles.wrapper} ${styles.collapsed}`
wrapper.className = styles.wrapper
wrapper.setAttribute('data-browser-use-ignore', 'true')
wrapper.innerHTML = `
<div class="${styles.background}"></div>
<div class="${styles.historySectionWrapper}">
<div class="${styles.historySection}">
${this.#createHistoryItem({
id: 'placeholder',
stepNumber: 0,
timestamp: new Date(),
type: 'thinking',
displayText: this.#i18n.t('ui.panel.waitingPlaceholder'),
})}
<div class="${styles.historyItem}">
<div class="${styles.historyContent}">
<span class="${styles.statusIcon}">🧠</span>
<span>${this.#i18n.t('ui.panel.waitingPlaceholder')}</span>
</div>
</div>
</div>
</div>
<div class="${styles.header}">
@@ -426,9 +383,6 @@ export class Panel {
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
</button>
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#i18n.t('ui.panel.pause')}">
</button>
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
X
</button>
@@ -466,12 +420,6 @@ export class Panel {
this.#toggle()
})
// Pause/continue button
this.#pauseButton.addEventListener('click', (e) => {
e.stopPropagation()
this.#togglePause()
})
// Stop button
this.#stopButton.addEventListener('click', (e) => {
e.stopPropagation()
@@ -503,7 +451,6 @@ export class Panel {
#expand(): void {
this.#isExpanded = true
this.wrapper.classList.remove(styles.collapsed)
this.wrapper.classList.add(styles.expanded)
this.#expandButton.textContent = '▲'
}
@@ -511,7 +458,6 @@ export class Panel {
#collapse(): void {
this.#isExpanded = false
this.wrapper.classList.remove(styles.expanded)
this.wrapper.classList.add(styles.collapsed)
this.#expandButton.textContent = '▼'
}
@@ -580,7 +526,9 @@ export class Panel {
}, 150) // Half the duration of fade out animation
}
#updateStatusIndicator(type: Step['type']): void {
#updateStatusIndicator(
type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'
): void {
// Clear all status classes
this.#indicator.className = styles.indicator
@@ -588,15 +536,6 @@ export class Panel {
this.#indicator.classList.add(styles[type])
}
#updateHistory(): void {
const steps = this.#state.getAllSteps()
this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('')
// Scroll to bottom to show latest records
this.#scrollToBottom()
}
#scrollToBottom(): void {
// Execute in next event loop to ensure DOM update completion
setTimeout(() => {
@@ -604,68 +543,116 @@ export class Panel {
}, 0)
}
#createHistoryItem(step: Step): string {
const time = step.timestamp.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
/**
* Render history directly from agent.history
*
* Renders:
* 1. Task (first item, from agent.task)
* 2. Reflection cards (evaluation, memory, next_goal)
* 3. Tool execution with output
* 4. Observations
*/
#renderHistory(): void {
const items: string[] = []
let typeClass = ''
let statusIcon = ''
// Set styles and icons based on step type
if (step.type === 'completed') {
// Check if this is a result from done tool
if (step.toolName === 'done') {
// Judge success or failure based on result
const failureKeyword = this.#i18n.t('ui.tools.resultFailure')
const errorKeyword = this.#i18n.t('ui.tools.resultError')
const isSuccess =
!step.toolResult ||
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
typeClass = isSuccess ? styles.doneSuccess : styles.doneError
statusIcon = isSuccess ? '🎉' : '❌'
} else {
typeClass = styles.completed
statusIcon = '✅'
}
} else if (step.type === 'error') {
typeClass = styles.error
statusIcon = '❌'
} else if (step.type === 'tool_executing') {
statusIcon = '⚙️'
} else if (step.type === 'output') {
typeClass = styles.output
statusIcon = '🤖'
} else if (step.type === 'input') {
typeClass = styles.input
statusIcon = '🎯'
} else if (step.type === 'retry') {
typeClass = styles.retry
statusIcon = '🔄'
} else {
statusIcon = '🧠'
// 1. Task card (always first)
const task = this.#agent.task
if (task) {
items.push(this.#createTaskCard(task))
}
const durationText = step.duration ? ` · ${step.duration}ms` : ''
const stepLabel = this.#i18n.t('ui.panel.step', {
number: step.stepNumber.toString(),
time,
duration: durationText || '', // Explicitly pass empty string to replace template
})
// 2. Render each history event
const history = this.#agent.history
for (const event of history) {
items.push(...this.#createHistoryCards(event))
}
return `
<div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}">
<span class="${styles.statusIcon}">${statusIcon}</span>
<span>${step.displayText}</span>
</div>
<div class="${styles.historyMeta}">
${stepLabel}
</div>
</div>
`
this.#historySection.innerHTML = items.join('')
this.#scrollToBottom()
}
#createTaskCard(task: string): string {
return createCard({ icon: '🎯', content: task, type: 'input' })
}
/** Create cards for a history event */
#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {
const cards: string[] = []
const time = formatTime(this.#config.language ?? 'en-US')
const meta =
event.type === 'step' && event.stepIndex !== undefined
? this.#i18n.t('ui.panel.step', {
number: (event.stepIndex + 1).toString(),
time,
duration: '',
})
: time
if (event.type === 'step') {
// Reflection card
if (event.reflection) {
const lines = createReflectionLines(event.reflection)
if (lines.length > 0) {
cards.push(createCard({ icon: '🧠', content: lines, meta }))
}
}
// Action card
const action = event.action
if (action) {
cards.push(...this.#createActionCards(action, meta))
}
} else if (event.type === 'observation') {
cards.push(
createCard({ icon: '👁️', content: event.content || '', meta, type: 'observation' })
)
} else if (event.type === 'user_takeover') {
cards.push(createCard({ icon: '👤', content: 'User takeover', meta, type: 'input' }))
} else if (event.type === 'retry') {
const retryInfo = `${event.message || 'Retrying'} (${event.attempt}/${event.maxAttempts})`
cards.push(createCard({ icon: '🔄', content: retryInfo, meta, type: 'observation' }))
} else if (event.type === 'error') {
cards.push(
createCard({ icon: '❌', content: event.message || 'Error', meta, type: 'observation' })
)
}
return cards
}
/** Create cards for an action */
#createActionCards(
action: { name: string; input: unknown; output: string },
meta: string
): string[] {
const cards: string[] = []
if (action.name === 'done') {
const input = action.input as { text?: string }
const text = input.text || action.output || ''
if (text) {
cards.push(createCard({ icon: '🤖', content: text, meta, type: 'output' }))
}
} else if (action.name === 'ask_user') {
const input = action.input as { question?: string }
const answer = action.output.replace(/^User answered:\s*/i, '')
cards.push(
createCard({
icon: '❓',
content: `Question: ${input.question || ''}`,
meta,
type: 'question',
})
)
cards.push(createCard({ icon: '💬', content: `Answer: ${answer}`, meta, type: 'input' }))
} else {
const toolText = this.#getToolExecutingText(action.name, action.input)
cards.push(createCard({ icon: '🔨', content: toolText, meta }))
if (action.output?.length > 0) {
cards.push(createCard({ icon: '🔨', content: action.output, meta, type: 'output' }))
}
}
return cards
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Card HTML generation utilities for Panel
*/
import { escapeHtml } from '../utils'
import styles from './Panel.module.css'
type CardType = 'default' | 'input' | 'output' | 'question' | 'observation'
interface CardOptions {
icon: string
content: string | string[]
meta?: string
type?: CardType
}
/** Create a single history card */
export function createCard({ icon, content, meta, type }: CardOptions): string {
const typeClass = type ? styles[type] : ''
const contentHtml = Array.isArray(content)
? `<div class="${styles.reflectionLines}">${content.join('')}</div>`
: `<span>${escapeHtml(content)}</span>`
return `
<div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}">
<span class="${styles.statusIcon}">${icon}</span>
${contentHtml}
</div>
${meta ? `<div class="${styles.historyMeta}">${meta}</div>` : ''}
</div>
`
}
/** Format timestamp for cards */
export function formatTime(locale: string = 'en-US'): string {
return new Date().toLocaleTimeString(locale, {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
/** Create reflection lines from reflection object */
export function createReflectionLines(reflection: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}): string[] {
const lines: string[] = []
if (reflection.evaluation_previous_goal) {
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`)
}
if (reflection.memory) {
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`)
}
if (reflection.next_goal) {
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`)
}
return lines
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*
* Events dispatched: CustomEvent<AgentActivity>
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
/**
* Minimal interface that Panel expects from an agent.
* Panel does not depend on PageAgent directly - it only requires this interface.
* This enables decoupling and allows any agent implementation to work with Panel.
*
* Events:
* - 'statuschange': Agent status changed (idle/running/completed/error)
* - 'historychange': Historical events updated (persisted)
* - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)
* - 'dispose': Agent is being disposed
*/
export interface PanelAgentAdapter extends EventTarget {
/** Current agent status */
readonly status: 'idle' | 'running' | 'completed' | 'error'
/** History of agent events */
readonly history: readonly {
type: 'step' | 'observation' | 'user_takeover' | 'retry' | 'error'
stepIndex?: number
/** For 'step' type */
reflection?: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}
/** For 'step' type */
action?: {
name: string
input: unknown
output: string
}
/** For 'observation' type */
content?: string
/** For 'retry' type */
attempt?: number
maxAttempts?: number
/** For 'retry' and 'error' types */
message?: string
}[]
/** Current task being executed */
readonly task: string
/**
* Callback for when agent needs user input.
* Panel will set this to handle user questions via its UI.
*/
onAskUser?: (question: string) => Promise<string>
/** Execute a task */
execute(task: string): Promise<unknown>
/** Dispose the agent */
dispose(): void
}
+12
View File
@@ -4,3 +4,15 @@ export function truncate(text: string, maxLength: number): string {
}
return text
}
/**
* Escape HTML special characters to prevent XSS and rendering issues
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
+7 -8
View File
@@ -1,10 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+10 -11
View File
@@ -1,13 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default defineConfig({
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['ai-motion'],
external: [],
},
minify: false,
sourcemap: true,
+16 -21
View File
@@ -37,14 +37,17 @@ Available Magic UI components: https://magicui.design/docs/components
Located in `src/components/ui/`:
**From shadcn/ui:**
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
**From Magic UI:**
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
- `sparkles-text`, `text-animate`, `typing-animation`
**Custom:**
- `highlighter`, `kbd`, `spinner`
### Styling Rules
@@ -58,17 +61,17 @@ Located in `src/components/ui/`:
```
src/
├── pages/
│ ├── Home.tsx # Homepage
│ └── docs/
│ ├── Layout.tsx # Documentation sidebar
│ └── [section]/[topic]/page.tsx
├── components/
│ ├── ui/ # shadcn/ui + Magic UI components
│ ├── Header.tsx # Site header
── Footer.tsx # Site footer
│ └── DocsLayout.tsx # Documentation sidebar
├── docs/ # Documentation pages
│ └── [section]/[topic]/page.tsx
├── test-pages/ # Library integration tests
── Footer.tsx # Site footer
├── i18n/ # Internationalization
├── router.tsx # Central routing
├── page.tsx # Homepage
└── main.tsx # App entry
```
@@ -76,14 +79,9 @@ src/
### Documentation Page
1. Create `src/docs/<section>/<slug>/page.tsx`
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `DocsLayout.tsx`
### Test Page
1. Create `src/test-pages/<name>.tsx`
2. Add route to `src/test-pages/router.tsx`
3. Add navigation item to `pages/docs/Layout.tsx`
## Routing
@@ -93,18 +91,16 @@ Uses hash-based routing for static hosting:
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
<Router hook={useHashLocation}>
{/* routes */}
</Router>
;<Router hook={useHashLocation}>{/* routes */}</Router>
```
## Configuration Files
| File | Purpose |
|------|---------|
| File | Purpose |
| ----------------- | ----------------------- |
| `components.json` | shadcn/ui configuration |
| `vite.config.js` | Vite build settings |
| `tsconfig.json` | TypeScript config |
| `vite.config.js` | Vite build settings |
| `tsconfig.json` | TypeScript config |
## Commands
@@ -112,4 +108,3 @@ import { useHashLocation } from 'wouter/use-hash-location'
npm start # Dev server (from root)
npm run build:website # Build website (from root)
```
-1
View File
@@ -1 +0,0 @@
# Landing Page & Docs
+22 -22
View File
@@ -1,24 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
+40 -40
View File
@@ -1,42 +1,42 @@
{
"name": "@page-agent/website",
"private": true,
"version": "0.0.16",
"type": "module",
"scripts": {
"dev": "vite",
"build:website": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.0",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"motion": "^12.23.26",
"next-themes": "^0.4.6",
"rough-notation": "^0.5.1",
"simple-icons": "^16.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
}
"name": "@page-agent/website",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build:website": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.3",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"motion": "^12.27.1",
"next-themes": "^0.4.6",
"rough-notation": "^0.5.1",
"simple-icons": "^16.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
}
}
+8 -4
View File
@@ -22,7 +22,7 @@ export default function Header() {
<div className="flex items-center justify-between gap-2">
{/* Logo */}
<Link
href="/"
href="~/"
className="flex items-center gap-2 sm:gap-3 group shrink-0"
aria-label={t('header.logo_alt')}
onClick={() => setMobileMenuOpen(false)}
@@ -41,6 +41,7 @@ export default function Header() {
className="hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible"
duration={600}
animateOnHover={true}
aria-hidden="true"
>
{t('header.slogan')}
</HyperText>
@@ -54,7 +55,7 @@ export default function Header() {
aria-label="Mobile navigation"
>
<Link
href="/docs/introduction/overview"
href="~/docs/introduction/overview"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_docs')}
>
@@ -84,8 +85,11 @@ export default function Header() {
role="navigation"
aria-label={t('header.nav_docs')}
>
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
</span>
<Link
href="/docs/introduction/overview"
href="~/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
<BookOpen className="w-4 h-4" />
@@ -133,7 +137,7 @@ export default function Header() {
role="navigation"
>
<Link
href="/docs/introduction/overview"
href="~/docs/introduction/overview"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
@@ -0,0 +1,167 @@
/**
* API Reference component for displaying TypeScript interface definitions
*
* Provides a beautiful, readable table for documenting API interfaces
*/
import * as React from 'react'
import { cn } from '@/lib/utils'
import { Badge } from './badge'
// ============================================================================
// Types
// ============================================================================
export interface PropDefinition {
/** Property name */
name: string
/** TypeScript type (can include generics, unions, etc.) */
type: string
/** Whether the property is required */
required?: boolean
/** Default value if any */
defaultValue?: string
/** Description of the property */
description: React.ReactNode
/** Mark as experimental/deprecated */
status?: 'experimental' | 'deprecated'
}
export interface APIReferenceProps {
/** Title for the API section */
title?: string
/** Optional description */
description?: React.ReactNode
/** Property definitions */
properties: PropDefinition[]
/** Additional CSS classes */
className?: string
}
// ============================================================================
// Component
// ============================================================================
export function APIReference({ title, description, properties, className }: APIReferenceProps) {
return (
<div className={cn('my-6', className)}>
{title && (
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">{title}</h3>
)}
{description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{description}</p>
)}
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800/50">
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
Property
</th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
Type
</th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell">
Default
</th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
Description
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{properties.map((prop) => (
<PropRow key={prop.name} {...prop} />
))}
</tbody>
</table>
</div>
</div>
)
}
function PropRow({ name, type, required, defaultValue, description, status }: PropDefinition) {
return (
<tr className="bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
{/* Property name */}
<td className="px-4 py-3 align-top">
<div className="flex items-center gap-2 flex-wrap">
<code className="font-mono text-sm font-medium text-indigo-600 dark:text-indigo-400">
{name}
</code>
{required && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 border-red-300 text-red-600 dark:border-red-800 dark:text-red-400"
>
required
</Badge>
)}
{status === 'experimental' && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 border-amber-300 text-amber-600 dark:border-amber-800 dark:text-amber-400"
>
experimental
</Badge>
)}
{status === 'deprecated' && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 border-gray-300 text-gray-500 dark:border-gray-700 dark:text-gray-500 line-through"
>
deprecated
</Badge>
)}
</div>
</td>
{/* Type */}
<td className="px-4 py-3 align-top">
<code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded whitespace-nowrap">
{type}
</code>
</td>
{/* Default value */}
<td className="px-4 py-3 align-top hidden md:table-cell">
{defaultValue ? (
<code className="font-mono text-xs text-gray-600 dark:text-gray-400">{defaultValue}</code>
) : (
<span className="text-gray-400 dark:text-gray-600">-</span>
)}
</td>
{/* Description */}
<td className="px-4 py-3 align-top text-gray-600 dark:text-gray-400">{description}</td>
</tr>
)
}
// ============================================================================
// Utility Components
// ============================================================================
/** Code inline span for type references in descriptions */
export function TypeRef({ children }: { children: React.ReactNode }) {
return (
<code className="font-mono text-xs text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/50 px-1 py-0.5 rounded">
{children}
</code>
)
}
/** Section divider for grouping related APIs */
export function APIDivider({ title }: { title: string }) {
return (
<div className="flex items-center gap-4 my-8">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent" />
<span className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{title}
</span>
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent" />
</div>
)
}
+11 -3
View File
@@ -1,3 +1,11 @@
export const CDN_URL = 'https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js'
export const CDN_CN_URL =
'https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js'
// Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js'
export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js'
// Demo LLM for website testing
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEMO_BASE_URL =
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
@@ -1,48 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function DataMasking() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
AI
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔒
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
</div>
<CodeEditor
code={`// 数据脱敏配置
// @todo
const rules = [
{ pattern: /\\d{11}/, replacement: '***-****-****' },
{ pattern: /\\d{4}-\\d{4}-\\d{4}-\\d{4}/, replacement: '****-****-****-****' }
]
pageAgent.maskData(rules)`}
/>
</div>
)
}
@@ -1,162 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function KnowledgeInjection() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
AI
</p>
{/* Custom Instruction */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Instruction - </h2>
<div className="p-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
🎯
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<ul className="list-disc list-inside space-y-2 text-gray-500 dark:text-gray-400">
<li> AI </li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<CodeEditor
className="mb-6"
code={`// 构造函数中设置系统指令
const pageAgent = new PageAgent({
instruction: \`
# 角色定义
你是专业的电商运营助手。
# 工作风格
- 谨慎:操作前确认
- 准确:确保正确性
- 高效:优化流程
# 错误处理
遇到错误时暂停并报告。
\`
});`}
/>
</section>
{/* App Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">App Knowledge - </h2>
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 应用知识
pageAgent.knowledge.setAppKnowledge(\`
# 产品介绍
电商管理系统:面向中小企业的一站式解决方案。
# 操作流程
## 商品上架
1. 进入商品管理页面 2. 点击新增商品 3. 填写基本信息 4. 设置库存 5. 提交审核
# 术语解释
- SKU:库存量单位
- SPU:标准产品单位
- 运费模板:物流费用计算规则
# 业务规则
- 库存为0时自动下架
- VIP会员享9.5折
\`);`}
/>
</section>
{/* Page Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Page Knowledge - </h2>
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
📄
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 页面知识库
// 添加页面知识
pageAgent.knowledge.addPageKnowledge("/products", \`
商品列表页面,包含搜索、筛选、批量操作功能。
#add-product-btn:新增商品按钮
.product-item:商品列表项
#search-input:搜索框,最少2个字符
\`);
pageAgent.knowledge.addPageKnowledge("/orders/*", \`
订单详情页面。
.order-status:订单状态标签
#update-status-btn:状态更新按钮
\`);
// 移除页面知识
pageAgent.knowledge.removePageKnowledge("/products");`}
/>
</section>
</div>
)
}
@@ -1,81 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
export default function SecurityPermissions() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
page-agent AI
</p>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫
</h3>
<p className="text-gray-600 dark:text-gray-300">
AI
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI </p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">URL </h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">Instruction </h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3">
AI
</p>
<div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
</section>
</div>
</div>
)
}
@@ -1,44 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function CdnSetup() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">CDN </h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
CDN page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor
className="mb-8"
code={`
// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script
src="${CDN_URL}"
crossorigin="true"
type="text/javascript"
></script>
<script>
// window.pageAgent
</script>`}
/>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
<li> 使</li>
<li> HTTPS 使</li>
<li> CSP </li>
</ul>
</div>
</div>
)
}
@@ -1,116 +0,0 @@
import CodeEditor from '@/components/CodeEditor'
export default function Configuration() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<CodeEditor
className="mb-8"
language="typescript"
code={`// config
type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
* The response should follow OpenAI API format.
*/
customFetch?: typeof globalThis.fetch
}
interface AgentConfig {
language?: "en-US" | "zh-CN"
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see [tools](../tools/index.ts)
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
// lifecycle hooks
// @todo: use event instead of hooks
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/**
* @note this hook can block the disposal process
* @note when dispose caused by page unload, "reason" will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
*/
onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
experimentalPreventNewPage?: boolean
}
interface PageControllerConfig {
interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[]
include_attributes?: string[]
highlightOpacity?: number
highlightLabelOpacity?: number
viewportExpansion?: number
}
`}
/>
</div>
)
}
@@ -1,84 +0,0 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function QuickStart() {
const { t } = useTranslation('docs')
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('quick_start.title')}</h1>
<p className=" mb-6 leading-relaxed">{t('quick_start.subtitle')}</p>
<h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{t('quick_start.step1_title')}
</h3>
<div className="space-y-3">
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
<CodeEditor
code={`// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script src="${CDN_URL}" crossorigin="true" type="text/javascript"></script>`}
language="html"
/>
</div>
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_npm')}</p>
<CodeEditor
code={`// npm install page-agent
import PageAgent from 'page-agent'`}
language="bash"
/>
</div>
</div>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('quick_start.step2_title')}
</h3>
<CodeEditor
code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN'
})`}
language="javascript"
/>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('quick_start.step3_title')}
</h3>
<CodeEditor
code={`// 程序化执行自然语言指令
await pageAgent.execute('点击提交按钮,然后填写用户名为张三');
// 或者显示对话框让用户输入指令
pageAgent.panel.show()
`}
language="javascript"
/>
</div>
</div>
</div>
)
}
+4
View File
@@ -1,5 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VERSION: string
}
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
+3 -4
View File
@@ -28,9 +28,9 @@ pages/i18n/
import { useTranslation } from 'react-i18next'
function MyComponent() {
const { t } = useTranslation('common') // 指定命名空间
return <h1>{t('header.nav_docs')}</h1>
const { t } = useTranslation('common') // 指定命名空间
return <h1>{t('header.nav_docs')}</h1>
}
```
@@ -71,4 +71,3 @@ t('common:header.nav_docs')
- [ ] 文档页翻译(`docs.json`
- [ ] DocsLayout 导航结构国际化
- [ ] 404 页面国际化
@@ -24,17 +24,19 @@ export default {
introduction: 'Introduction',
features: 'Features',
integration: 'Integration',
advanced: 'Advanced',
overview: 'Overview',
quick_start: 'Quick Start',
limitations: 'Limitations',
model_integration: 'Model Integration',
models: 'Models',
custom_tools: 'Custom Tools',
knowledge_injection: 'Knowledge Injection',
security_permissions: 'Security & Permissions',
knowledge_injection: 'Instructions',
data_masking: 'Data Masking',
cdn_setup: 'CDN Setup',
configuration: 'Configuration',
best_practices: 'Best Practices',
third_party_agent: 'Third-party Agent',
security_permissions: 'Security & Permissions',
page_agent: 'PageAgent',
page_agent_core: 'PageAgentCore',
},
}
@@ -5,7 +5,7 @@ export default {
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
what_is: 'What is page-agent?',
what_is_desc:
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or agent clients builders. Integrate it into your site to let users interact with pages through natural language.',
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.',
features_title: 'Core Features',
feature_dom: {
title: '🧠 Smart DOM Analysis',
@@ -112,8 +112,8 @@ export default {
future_3: '• Basic visual understanding',
future_4: '• Smarter error recovery',
},
model_integration: {
title: 'Model Integration',
models: {
title: 'Models',
subtitle:
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
@@ -23,17 +23,19 @@ export default {
introduction: '介绍',
features: '功能特性',
integration: '集成指南',
advanced: '高级',
overview: '概览',
quick_start: '快速开始',
limitations: '使用限制',
model_integration: '模型接入',
models: '模型',
custom_tools: '自定义工具',
knowledge_injection: '知识注入',
security_permissions: '安全与权限',
knowledge_injection: '知识注入',
data_masking: '数据脱敏',
cdn_setup: 'CDN 引入',
configuration: '配置选项',
best_practices: '最佳实践',
third_party_agent: '接入第三方 Agent',
security_permissions: '安全与权限',
page_agent: 'PageAgent',
page_agent_core: 'PageAgentCore',
},
}
@@ -111,8 +111,8 @@ export default {
future_3: '• 基础的视觉理解能力',
future_4: '• 更智能的错误恢复机制',
},
model_integration: {
title: '模型接入',
models: {
title: '模型',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
recommended: '推荐模型',
+2 -6
View File
@@ -1,19 +1,15 @@
import { createRoot } from 'react-dom/client'
import { Route, Router, Switch } from 'wouter'
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { default as PagesRouter } from './router'
import { default as TestPagesRouter } from './test-pages/router'
import './index.css'
createRoot(document.getElementById('root')!).render(
<Router hook={useHashLocation}>
<Switch>
<Route path="/test-pages" component={TestPagesRouter} nest />
<Route path="/" component={PagesRouter} nest />
</Switch>
<PagesRouter />
</Router>
)

Some files were not shown because too many files have changed in this diff Show More