@handstage/agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/definitions.js +209 -0
- package/dist/handlerTypes.js +0 -0
- package/dist/handlers.js +104 -0
- package/dist/index.js +4 -0
- package/dist/result.js +21 -0
- package/dist/schemas.js +168 -0
- package/dist/types.js +0 -0
- package/package.json +32 -0
- package/src/definitions.ts +263 -0
- package/src/handlerTypes.ts +83 -0
- package/src/handlers.ts +163 -0
- package/src/index.ts +10 -0
- package/src/result.ts +31 -0
- package/src/schemas.ts +207 -0
- package/src/types.ts +108 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { type ToolSet, tool } from "ai"
|
|
2
|
+
import type { HandstageAgentToolHandlers } from "./handlerTypes"
|
|
3
|
+
import {
|
|
4
|
+
// ClickInputSchema,
|
|
5
|
+
ClickOnIdInputSchema,
|
|
6
|
+
// BringToFrontInputSchema,
|
|
7
|
+
// BringToFrontOutputSchema,
|
|
8
|
+
ClosePageInputSchema,
|
|
9
|
+
ClosePageOutputSchema,
|
|
10
|
+
// ClickOnInputSchema,
|
|
11
|
+
ElementActionOutputSchema,
|
|
12
|
+
FillOnIdInputSchema,
|
|
13
|
+
// FillOnInputSchema,
|
|
14
|
+
GoBackInputSchema,
|
|
15
|
+
GoForwardInputSchema,
|
|
16
|
+
GotoInputSchema,
|
|
17
|
+
GotoOutputSchema,
|
|
18
|
+
HistoryNavOutputSchema,
|
|
19
|
+
// HoverInputSchema,
|
|
20
|
+
HoverOnIdInputSchema,
|
|
21
|
+
// HoverOnInputSchema,
|
|
22
|
+
NewPageInputSchema,
|
|
23
|
+
NewPageOutputSchema,
|
|
24
|
+
// PageInfoInputSchema,
|
|
25
|
+
// PageInfoOutputSchema,
|
|
26
|
+
PagesInputSchema,
|
|
27
|
+
PagesOutputSchema,
|
|
28
|
+
// PointerOutputSchema,
|
|
29
|
+
ReloadInputSchema,
|
|
30
|
+
ReloadOutputSchema,
|
|
31
|
+
// ScrollInputSchema,
|
|
32
|
+
SnapshotDomInputSchema,
|
|
33
|
+
SnapshotDomOutputSchema,
|
|
34
|
+
// TypeInputSchema,
|
|
35
|
+
TypeOnIdInputSchema,
|
|
36
|
+
} from "./schemas"
|
|
37
|
+
|
|
38
|
+
export type HandstageAgentToolSet = ReturnType<
|
|
39
|
+
typeof createHandstageAgentToolDefinitions
|
|
40
|
+
>
|
|
41
|
+
|
|
42
|
+
function pagesToXml(
|
|
43
|
+
pages: Array<{ pageId: string; url: string; title: string }>,
|
|
44
|
+
): string {
|
|
45
|
+
return `<pages>\n${pages
|
|
46
|
+
.map(
|
|
47
|
+
(page) =>
|
|
48
|
+
`<page>\n<pageId>${page.pageId}</pageId>\n<url>${page.url}</url>\n<title>${page.title}</title>\n</page>`,
|
|
49
|
+
)
|
|
50
|
+
.join("\n")}\n</pages>`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Same object as {@link handstageAgentTools}; kept for callers that only need a `ToolSet`.
|
|
55
|
+
*/
|
|
56
|
+
export function createHandstageAgentToolDefinitions(
|
|
57
|
+
handler: HandstageAgentToolHandlers,
|
|
58
|
+
) {
|
|
59
|
+
const tools = {
|
|
60
|
+
pages: tool({
|
|
61
|
+
description:
|
|
62
|
+
"List opend pages in current browser context. Each entry has pageId, url, and title. Pages are returned in the order they were opened; no tab is implicitly 'active' — pass the pageId you want to act on with every tool call.",
|
|
63
|
+
inputSchema: PagesInputSchema,
|
|
64
|
+
outputSchema: PagesOutputSchema,
|
|
65
|
+
toModelOutput: ({ output }) => {
|
|
66
|
+
return {
|
|
67
|
+
type: "text",
|
|
68
|
+
value: pagesToXml(output.pages),
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
execute: async (input) => {
|
|
72
|
+
return await handler.pages(input)
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
newPage: tool({
|
|
77
|
+
description:
|
|
78
|
+
"Open a new browser page. Returns the new page's pageId. Optional starting URL (defaults to about:blank).",
|
|
79
|
+
inputSchema: NewPageInputSchema,
|
|
80
|
+
outputSchema: NewPageOutputSchema,
|
|
81
|
+
execute: async (input) => {
|
|
82
|
+
return await handler.newPage(input)
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
closePage: tool({
|
|
87
|
+
description: "Close a browser page by pageId.",
|
|
88
|
+
inputSchema: ClosePageInputSchema,
|
|
89
|
+
outputSchema: ClosePageOutputSchema,
|
|
90
|
+
execute: async (input) => {
|
|
91
|
+
return await handler.closePage(input)
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
// bringToFront: tool({
|
|
96
|
+
// description:
|
|
97
|
+
// "Foreground a tab by pageId (Target.activateTarget). Necessary before input events can land on that tab in headful Chrome.",
|
|
98
|
+
// inputSchema: BringToFrontInputSchema,
|
|
99
|
+
// outputSchema: BringToFrontOutputSchema,
|
|
100
|
+
// }),
|
|
101
|
+
|
|
102
|
+
goto: tool({
|
|
103
|
+
description: "Navigate a page to a URL.",
|
|
104
|
+
inputSchema: GotoInputSchema,
|
|
105
|
+
outputSchema: GotoOutputSchema,
|
|
106
|
+
execute: async (input) => {
|
|
107
|
+
return await handler.goto(input)
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
reload: tool({
|
|
112
|
+
description: "Reload the current document in a page.",
|
|
113
|
+
inputSchema: ReloadInputSchema,
|
|
114
|
+
outputSchema: ReloadOutputSchema,
|
|
115
|
+
execute: async (input) => {
|
|
116
|
+
return await handler.reload(input)
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
goBack: tool({
|
|
121
|
+
description: "Go back in history for a page, if possible.",
|
|
122
|
+
inputSchema: GoBackInputSchema,
|
|
123
|
+
outputSchema: HistoryNavOutputSchema,
|
|
124
|
+
execute: async (input) => {
|
|
125
|
+
return await handler.goBack(input)
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
goForward: tool({
|
|
130
|
+
description: "Go forward in history for a page, if possible.",
|
|
131
|
+
inputSchema: GoForwardInputSchema,
|
|
132
|
+
outputSchema: HistoryNavOutputSchema,
|
|
133
|
+
execute: async (input) => {
|
|
134
|
+
return await handler.goForward(input)
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
snapshot_dom: tool({
|
|
139
|
+
description:
|
|
140
|
+
"Accessibility tree for a page (pageId). Multiline outline with encoded node ids in brackets (e.g. [1-42]); use those ids with click_on_id, fill_on_id, type_on_id, or hover_on_id.",
|
|
141
|
+
inputSchema: SnapshotDomInputSchema,
|
|
142
|
+
outputSchema: SnapshotDomOutputSchema,
|
|
143
|
+
toModelOutput: ({ output }) => {
|
|
144
|
+
if (output.ok) {
|
|
145
|
+
return {
|
|
146
|
+
type: "text",
|
|
147
|
+
value: output.tree,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
type: "text",
|
|
152
|
+
value: output.error,
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
execute: async (input) => {
|
|
156
|
+
return await handler.snapshot_dom(input)
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// pageInfo: tool({
|
|
161
|
+
// description: "Current URL and document title for a page.",
|
|
162
|
+
// inputSchema: PageInfoInputSchema,
|
|
163
|
+
// outputSchema: PageInfoOutputSchema,
|
|
164
|
+
// }),
|
|
165
|
+
|
|
166
|
+
// click: tool({
|
|
167
|
+
// description:
|
|
168
|
+
// "Click at viewport coordinates (CSS pixels). Does not scroll; ensure the target is visible.",
|
|
169
|
+
// inputSchema: ClickInputSchema,
|
|
170
|
+
// outputSchema: PointerOutputSchema,
|
|
171
|
+
// }),
|
|
172
|
+
|
|
173
|
+
// hover: tool({
|
|
174
|
+
// description: "Move the pointer to viewport coordinates (CSS pixels).",
|
|
175
|
+
// inputSchema: HoverInputSchema,
|
|
176
|
+
// outputSchema: PointerOutputSchema,
|
|
177
|
+
// }),
|
|
178
|
+
|
|
179
|
+
// scroll: tool({
|
|
180
|
+
// description:
|
|
181
|
+
// "Dispatch a mouse wheel at viewport coordinates (deltaX/deltaY in pixels).",
|
|
182
|
+
// inputSchema: ScrollInputSchema,
|
|
183
|
+
// outputSchema: PointerOutputSchema,
|
|
184
|
+
// }),
|
|
185
|
+
|
|
186
|
+
// type: tool({
|
|
187
|
+
// description:
|
|
188
|
+
// "Type text using key events at the current focus. Focus an input first (e.g. click_on_id) or tab to it.",
|
|
189
|
+
// inputSchema: TypeInputSchema,
|
|
190
|
+
// outputSchema: TypeOutputSchema,
|
|
191
|
+
// }),
|
|
192
|
+
|
|
193
|
+
// click_on: tool({
|
|
194
|
+
// description:
|
|
195
|
+
// "Click the first element matching a CSS or XPath selector in the page's main frame.",
|
|
196
|
+
// inputSchema: ClickOnInputSchema,
|
|
197
|
+
// outputSchema: ElementActionOutputSchema,
|
|
198
|
+
// }),
|
|
199
|
+
|
|
200
|
+
// fill_on: tool({
|
|
201
|
+
// description:
|
|
202
|
+
// "Clear and fill an input element matched by a CSS or XPath selector (main frame).",
|
|
203
|
+
// inputSchema: FillOnInputSchema,
|
|
204
|
+
// outputSchema: ElementActionOutputSchema,
|
|
205
|
+
// }),
|
|
206
|
+
|
|
207
|
+
// type_on: tool({
|
|
208
|
+
// description:
|
|
209
|
+
// "Type into an element matched by a CSS or XPath selector (focuses the element first).",
|
|
210
|
+
// inputSchema: TypeOnInputSchema,
|
|
211
|
+
// outputSchema: ElementActionOutputSchema,
|
|
212
|
+
// }),
|
|
213
|
+
|
|
214
|
+
// hover_on: tool({
|
|
215
|
+
// description:
|
|
216
|
+
// "Hover the first element matching a CSS or XPath selector in the page's main frame.",
|
|
217
|
+
// inputSchema: HoverOnInputSchema,
|
|
218
|
+
// outputSchema: ElementActionOutputSchema,
|
|
219
|
+
// }),
|
|
220
|
+
|
|
221
|
+
click_on_id: tool({
|
|
222
|
+
description:
|
|
223
|
+
"Click the element for an encoded accessibility tree node id from snapshot_dom.",
|
|
224
|
+
inputSchema: ClickOnIdInputSchema,
|
|
225
|
+
outputSchema: ElementActionOutputSchema,
|
|
226
|
+
execute: async (input) => {
|
|
227
|
+
return await handler.click_on_id(input)
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
|
|
231
|
+
fill_on_id: tool({
|
|
232
|
+
description:
|
|
233
|
+
"Clear and fill an input for an encoded accessibility tree node id from snapshot_dom.",
|
|
234
|
+
inputSchema: FillOnIdInputSchema,
|
|
235
|
+
outputSchema: ElementActionOutputSchema,
|
|
236
|
+
execute: async (input) => {
|
|
237
|
+
return await handler.fill_on_id(input)
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
type_on_id: tool({
|
|
242
|
+
description:
|
|
243
|
+
"Type into an element for an encoded accessibility tree node id from snapshot_dom.",
|
|
244
|
+
inputSchema: TypeOnIdInputSchema,
|
|
245
|
+
outputSchema: ElementActionOutputSchema,
|
|
246
|
+
execute: async (input) => {
|
|
247
|
+
return await handler.type_on_id(input)
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
|
|
251
|
+
hover_on_id: tool({
|
|
252
|
+
description:
|
|
253
|
+
"Hover the element for an encoded accessibility tree node id from snapshot_dom.",
|
|
254
|
+
inputSchema: HoverOnIdInputSchema,
|
|
255
|
+
outputSchema: ElementActionOutputSchema,
|
|
256
|
+
execute: async (input) => {
|
|
257
|
+
return await handler.hover_on_id(input)
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
} as const satisfies ToolSet
|
|
261
|
+
|
|
262
|
+
return tools
|
|
263
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Page } from "@handstage/core"
|
|
2
|
+
import type {
|
|
3
|
+
// ClickInput,
|
|
4
|
+
ClickOnIdInput,
|
|
5
|
+
ClickOnIdOutput,
|
|
6
|
+
// BringToFrontInput,
|
|
7
|
+
// BringToFrontOutput,
|
|
8
|
+
ClosePageInput,
|
|
9
|
+
ClosePageOutput,
|
|
10
|
+
// ClickOutput,
|
|
11
|
+
FillOnIdInput,
|
|
12
|
+
FillOnIdOutput,
|
|
13
|
+
GoBackInput,
|
|
14
|
+
GoBackOutput,
|
|
15
|
+
GoForwardInput,
|
|
16
|
+
GoForwardOutput,
|
|
17
|
+
GotoInput,
|
|
18
|
+
GotoOutput,
|
|
19
|
+
// HoverInput,
|
|
20
|
+
HoverOnIdInput,
|
|
21
|
+
HoverOnIdOutput,
|
|
22
|
+
// HoverOutput,
|
|
23
|
+
NewPageInput,
|
|
24
|
+
NewPageOutput,
|
|
25
|
+
// PageInfoInput,
|
|
26
|
+
// PageInfoOutput,
|
|
27
|
+
PagesInput,
|
|
28
|
+
PagesOutput,
|
|
29
|
+
ReloadInput,
|
|
30
|
+
ReloadOutput,
|
|
31
|
+
// ScrollInput,
|
|
32
|
+
// ScrollOutput,
|
|
33
|
+
SnapshotDomInput,
|
|
34
|
+
SnapshotDomOutput,
|
|
35
|
+
// TypeInput,
|
|
36
|
+
TypeOnIdInput,
|
|
37
|
+
TypeOnIdOutput,
|
|
38
|
+
// TypeOutput,
|
|
39
|
+
} from "./types"
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Browser context exposed by Handstage (`Handstage.defaultBrowserContext()`). Implementations
|
|
43
|
+
* of {@link HandstageAgentToolHandlers} typically hold this.
|
|
44
|
+
*
|
|
45
|
+
* There is no implicit "active page" — callers track Page references they
|
|
46
|
+
* received from `newPage()` or `pages()` explicitly, and pass `pageId` on
|
|
47
|
+
* every tool call.
|
|
48
|
+
*/
|
|
49
|
+
export interface HandstageAgentContext {
|
|
50
|
+
pages(): Page[]
|
|
51
|
+
newPage(url?: string): Promise<Page>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Implementations perform Handstage actions for each tool. Inputs and outputs are
|
|
56
|
+
* inferred from Zod schemas in {@link ./schemas}.
|
|
57
|
+
*/
|
|
58
|
+
export interface HandstageAgentToolHandlers {
|
|
59
|
+
pages(input: PagesInput): Promise<PagesOutput>
|
|
60
|
+
newPage(input: NewPageInput): Promise<NewPageOutput>
|
|
61
|
+
closePage(input: ClosePageInput): Promise<ClosePageOutput>
|
|
62
|
+
// bringToFront(
|
|
63
|
+
// input: BringToFrontInput,
|
|
64
|
+
// ): Promise<BringToFrontOutput>
|
|
65
|
+
goto(input: GotoInput): Promise<GotoOutput>
|
|
66
|
+
reload(input: ReloadInput): Promise<ReloadOutput>
|
|
67
|
+
goBack(input: GoBackInput): Promise<GoBackOutput>
|
|
68
|
+
goForward(input: GoForwardInput): Promise<GoForwardOutput>
|
|
69
|
+
snapshot_dom(input: SnapshotDomInput): Promise<SnapshotDomOutput>
|
|
70
|
+
// pageInfo(input: PageInfoInput): Promise<PageInfoOutput>
|
|
71
|
+
// click(input: ClickInput): Promise<ClickOutput>
|
|
72
|
+
// hover(input: HoverInput): Promise<HoverOutput>
|
|
73
|
+
// scroll(input: ScrollInput): Promise<ScrollOutput>
|
|
74
|
+
// type(input: TypeInput): Promise<TypeOutput>
|
|
75
|
+
// click_on(input: ClickOnInput): Promise<ClickOnOutput>
|
|
76
|
+
// fill_on(input: FillOnInput): Promise<FillOnOutput>
|
|
77
|
+
// type_on(input: TypeOnInput): Promise<TypeOnOutput>
|
|
78
|
+
// hover_on(input: HoverOnInput): Promise<HoverOnOutput>
|
|
79
|
+
click_on_id(input: ClickOnIdInput): Promise<ClickOnIdOutput>
|
|
80
|
+
fill_on_id(input: FillOnIdInput): Promise<FillOnIdOutput>
|
|
81
|
+
type_on_id(input: TypeOnIdInput): Promise<TypeOnIdOutput>
|
|
82
|
+
hover_on_id(input: HoverOnIdInput): Promise<HoverOnIdOutput>
|
|
83
|
+
}
|
package/src/handlers.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Context, Page } from "@handstage/core"
|
|
2
|
+
import type { HandstageAgentToolHandlers } from "./handlerTypes"
|
|
3
|
+
import { errResult, tryAgentResult, withPage } from "./result"
|
|
4
|
+
import type {
|
|
5
|
+
ClickOnIdInput,
|
|
6
|
+
ClickOnIdOutput,
|
|
7
|
+
ClosePageInput,
|
|
8
|
+
ClosePageOutput,
|
|
9
|
+
FillOnIdInput,
|
|
10
|
+
FillOnIdOutput,
|
|
11
|
+
GoBackInput,
|
|
12
|
+
GoBackOutput,
|
|
13
|
+
GoForwardInput,
|
|
14
|
+
GoForwardOutput,
|
|
15
|
+
GotoInput,
|
|
16
|
+
GotoOutput,
|
|
17
|
+
HoverOnIdInput,
|
|
18
|
+
HoverOnIdOutput,
|
|
19
|
+
NewPageInput,
|
|
20
|
+
NewPageOutput,
|
|
21
|
+
PagesInput,
|
|
22
|
+
PagesOutput,
|
|
23
|
+
ReloadInput,
|
|
24
|
+
ReloadOutput,
|
|
25
|
+
SnapshotDomInput,
|
|
26
|
+
SnapshotDomOutput,
|
|
27
|
+
TypeOnIdInput,
|
|
28
|
+
TypeOnIdOutput,
|
|
29
|
+
} from "./types"
|
|
30
|
+
|
|
31
|
+
type DeepLocator = ReturnType<Page["deepLocator"]>
|
|
32
|
+
|
|
33
|
+
export class HandstageContextAgentToolHandlers
|
|
34
|
+
implements HandstageAgentToolHandlers
|
|
35
|
+
{
|
|
36
|
+
constructor(private readonly ctx: Context) {}
|
|
37
|
+
|
|
38
|
+
async pages(_input: PagesInput): Promise<PagesOutput> {
|
|
39
|
+
const pages = await Promise.all(
|
|
40
|
+
this.ctx.pages().map(async (page) => ({
|
|
41
|
+
pageId: page.pageId,
|
|
42
|
+
url: page.url(),
|
|
43
|
+
title: await page.title(),
|
|
44
|
+
})),
|
|
45
|
+
)
|
|
46
|
+
return { pages }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async newPage(input: NewPageInput): Promise<NewPageOutput> {
|
|
50
|
+
const page = await this.ctx.newPage(input.url ?? "about:blank")
|
|
51
|
+
return { pageId: page.pageId }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async closePage(input: ClosePageInput): Promise<ClosePageOutput> {
|
|
55
|
+
const page = this.ctx.resolvePageByTargetId(input.pageId)
|
|
56
|
+
if (!page) return errResult(`Unknown pageId: ${input.pageId}`)
|
|
57
|
+
return tryAgentResult(async () => {
|
|
58
|
+
await page.close()
|
|
59
|
+
return {}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async goto(input: GotoInput): Promise<GotoOutput> {
|
|
64
|
+
return withPage(this.ctx, input.pageId, async (page) => {
|
|
65
|
+
await page.goto(input.url, {
|
|
66
|
+
waitUntil: input.waitUntil,
|
|
67
|
+
timeoutMs: input.timeoutMs,
|
|
68
|
+
})
|
|
69
|
+
return { url: page.url() }
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async reload(input: ReloadInput): Promise<ReloadOutput> {
|
|
74
|
+
return withPage(this.ctx, input.pageId, async (page) => {
|
|
75
|
+
await page.reload({
|
|
76
|
+
waitUntil: input.waitUntil,
|
|
77
|
+
timeoutMs: input.timeoutMs,
|
|
78
|
+
ignoreCache: input.ignoreCache,
|
|
79
|
+
})
|
|
80
|
+
return { url: page.url() }
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async goBack(input: GoBackInput): Promise<GoBackOutput> {
|
|
85
|
+
return withPage(this.ctx, input.pageId, async (page) => {
|
|
86
|
+
const response = await page.goBack({
|
|
87
|
+
waitUntil: input.waitUntil,
|
|
88
|
+
timeoutMs: input.timeoutMs,
|
|
89
|
+
})
|
|
90
|
+
return { navigated: response !== null, url: page.url() }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async goForward(input: GoForwardInput): Promise<GoForwardOutput> {
|
|
95
|
+
return withPage(this.ctx, input.pageId, async (page) => {
|
|
96
|
+
const response = await page.goForward({
|
|
97
|
+
waitUntil: input.waitUntil,
|
|
98
|
+
timeoutMs: input.timeoutMs,
|
|
99
|
+
})
|
|
100
|
+
return { navigated: response !== null, url: page.url() }
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async snapshot_dom(input: SnapshotDomInput): Promise<SnapshotDomOutput> {
|
|
105
|
+
return withPage(this.ctx, input.pageId, async (page) => {
|
|
106
|
+
const snapshot = await page.snapshot({
|
|
107
|
+
includeIframes: input.includeIframes,
|
|
108
|
+
})
|
|
109
|
+
return {
|
|
110
|
+
tree: snapshot.formattedTree,
|
|
111
|
+
xpathMap: snapshot.xpathMap,
|
|
112
|
+
urlMap: snapshot.urlMap,
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async click_on_id(input: ClickOnIdInput): Promise<ClickOnIdOutput> {
|
|
118
|
+
return this.actOnEncodedId(input.pageId, input.id, (locator) =>
|
|
119
|
+
locator.click(),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async fill_on_id(input: FillOnIdInput): Promise<FillOnIdOutput> {
|
|
124
|
+
return this.actOnEncodedId(input.pageId, input.id, (locator) =>
|
|
125
|
+
locator.fill(input.value),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async type_on_id(input: TypeOnIdInput): Promise<TypeOnIdOutput> {
|
|
130
|
+
return this.actOnEncodedId(input.pageId, input.id, (locator) =>
|
|
131
|
+
locator.type(input.text, { delay: input.delay }),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async hover_on_id(input: HoverOnIdInput): Promise<HoverOnIdOutput> {
|
|
136
|
+
return this.actOnEncodedId(input.pageId, input.id, (locator) =>
|
|
137
|
+
locator.hover(),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async actOnEncodedId(
|
|
142
|
+
pageId: string,
|
|
143
|
+
encodedId: string,
|
|
144
|
+
action: (locator: DeepLocator) => Promise<void>,
|
|
145
|
+
): Promise<ClickOnIdOutput> {
|
|
146
|
+
return withPage(this.ctx, pageId, async (page) => {
|
|
147
|
+
const { xpathMap } = await page.snapshot()
|
|
148
|
+
const xpath = xpathMap[encodedId]
|
|
149
|
+
if (!xpath) {
|
|
150
|
+
throw new Error(`Unknown encoded id: ${encodedId}`)
|
|
151
|
+
}
|
|
152
|
+
await action(page.deepLocator(xpath))
|
|
153
|
+
await page.waitForLoadState("networkidle", 5000)
|
|
154
|
+
return {}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createHandstageContextAgentToolHandlers(
|
|
160
|
+
ctx: Context,
|
|
161
|
+
): HandstageAgentToolHandlers {
|
|
162
|
+
return new HandstageContextAgentToolHandlers(ctx)
|
|
163
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createHandstageAgentToolDefinitions,
|
|
3
|
+
type HandstageAgentToolSet,
|
|
4
|
+
} from "./definitions"
|
|
5
|
+
export {
|
|
6
|
+
createHandstageContextAgentToolHandlers,
|
|
7
|
+
HandstageContextAgentToolHandlers,
|
|
8
|
+
} from "./handlers"
|
|
9
|
+
export * from "./handlerTypes"
|
|
10
|
+
export * from "./schemas"
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Context, Page } from "@handstage/core"
|
|
2
|
+
import type { ErrResult } from "./types"
|
|
3
|
+
|
|
4
|
+
export function formatError(error: unknown): string {
|
|
5
|
+
return error instanceof Error ? error.message : String(error)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function errResult(error: string): ErrResult {
|
|
9
|
+
return { ok: false, error }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function tryAgentResult<T extends Record<string, unknown>>(
|
|
13
|
+
fn: () => Promise<T>,
|
|
14
|
+
): Promise<({ ok: true } & T) | ErrResult> {
|
|
15
|
+
try {
|
|
16
|
+
const data = await fn()
|
|
17
|
+
return { ok: true, ...data }
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return errResult(formatError(error))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function withPage<T extends Record<string, unknown>>(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
pageId: string,
|
|
26
|
+
fn: (page: Page) => Promise<T>,
|
|
27
|
+
): Promise<({ ok: true } & T) | ErrResult> {
|
|
28
|
+
const page = ctx.resolvePageByTargetId(pageId)
|
|
29
|
+
if (!page) return errResult(`Unknown pageId: ${pageId}`)
|
|
30
|
+
return tryAgentResult(() => fn(page))
|
|
31
|
+
}
|