Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/LOCAL-ENGINE-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,47 @@ An OpenAI chat-completions subset:
| `max_tokens` | no | |
| `temperature` | no | ignored for models that fix their own sampling |
| `reasoning_effort` | no | passed through where the provider accepts it |
| `redrob_context` | no | Redrob extension: `{ page_url, page_title }` of the page the caller is on — see Skill arming |

Unknown fields are ignored rather than rejected, so a newer client can talk to an
older engine without a version check.

### Skill arming

A skill arms itself — its body is added to the request without anyone loading it —
when its frontmatter declares `keywords:` or `urls:` (or the nested
`autoInject: { keywords, url }` block; both spellings merge):

```markdown
---
name: browser-gmail
description: Use when the task is reading or drafting mail in Gmail.
urls:
- https://mail.google.com/*
keywords:
- gmail
---
```

- **keywords** match the last `user` message, case-insensitively, on word boundaries
(`ai` does not match `said`). A keyword ending in Hangul needs no boundary after it,
so `홈택스` matches `홈택스에서`.
- **urls** match `redrob_context.page_url`. The scheme is ignored, `*` stays inside one
path segment, `**` crosses segments, and a trailing `/*` in `urls:` covers the whole
site including its root.
- Armed bodies go in as one extra `system` message after the caller's own system
messages, labelled with what armed each one. A skill with neither list never arms
and behaves as before; in agent sessions an armable skill is left out of the standing
skill list and appears only when it arms.

`redrob-browser` sends `redrob_context` on every task turn to the built-in engine: the
agent's own tab once it has opened one, else the http(s) tab the user started from. It
never sends it to a user's own provider. The engine ships ten site skills under
`packages/core/src/plugin/skill/browser/` (Gmail, Google Calendar, Notion, GitHub,
Slack, 네이버 메일, 네이버 검색, 카카오, 홈택스 조회, 쿠팡 주문 조회) and the
`skill-writer` skill, which saves a repeated instruction as a new
`~/.config/redrob/skills/<name>/SKILL.md`.

### Tool ownership is decided by the request

This is the whole design. One rule, no modes to configure:
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/generated-effect/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ type Endpoint24_0Input = {
readonly presence_penalty?: Endpoint24_0Request["payload"]["presence_penalty"]
readonly reasoning_effort?: Endpoint24_0Request["payload"]["reasoning_effort"]
readonly user?: Endpoint24_0Request["payload"]["user"]
readonly redrob_context?: Endpoint24_0Request["payload"]["redrob_context"]
}
const Endpoint24_0 = (raw: RawClient["server.chat"]) => (input: Endpoint24_0Input) =>
raw["chat.completions"]({
Expand All @@ -881,6 +882,7 @@ const Endpoint24_0 = (raw: RawClient["server.chat"]) => (input: Endpoint24_0Inpu
presence_penalty: input["presence_penalty"],
reasoning_effort: input["reasoning_effort"],
user: input["user"],
redrob_context: input["redrob_context"],
},
}).pipe(Effect.mapError(mapClientError))

Expand Down
1 change: 1 addition & 0 deletions packages/client/src/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ export function make(options: ClientOptions) {
presence_penalty: input["presence_penalty"],
reasoning_effort: input["reasoning_effort"],
user: input["user"],
redrob_context: input["redrob_context"],
},
successStatus: 200,
declaredStatuses: [401, 400],
Expand Down
91 changes: 91 additions & 0 deletions packages/client/src/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3167,6 +3167,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["model"]
readonly messages: {
readonly model: string
Expand Down Expand Up @@ -3210,6 +3213,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["messages"]
readonly tools?: {
readonly model: string
Expand Down Expand Up @@ -3253,6 +3259,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["tools"]
readonly tool_choice?: {
readonly model: string
Expand Down Expand Up @@ -3296,6 +3305,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["tool_choice"]
readonly stream?: {
readonly model: string
Expand Down Expand Up @@ -3339,6 +3351,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["stream"]
readonly max_tokens?: {
readonly model: string
Expand Down Expand Up @@ -3382,6 +3397,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["max_tokens"]
readonly max_completion_tokens?: {
readonly model: string
Expand Down Expand Up @@ -3425,6 +3443,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["max_completion_tokens"]
readonly temperature?: {
readonly model: string
Expand Down Expand Up @@ -3468,6 +3489,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["temperature"]
readonly top_p?: {
readonly model: string
Expand Down Expand Up @@ -3511,6 +3535,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["top_p"]
readonly stop?: {
readonly model: string
Expand Down Expand Up @@ -3554,6 +3581,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["stop"]
readonly seed?: {
readonly model: string
Expand Down Expand Up @@ -3597,6 +3627,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["seed"]
readonly frequency_penalty?: {
readonly model: string
Expand Down Expand Up @@ -3640,6 +3673,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["frequency_penalty"]
readonly presence_penalty?: {
readonly model: string
Expand Down Expand Up @@ -3683,6 +3719,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["presence_penalty"]
readonly reasoning_effort?: {
readonly model: string
Expand Down Expand Up @@ -3726,6 +3765,9 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["reasoning_effort"]
readonly user?: {
readonly model: string
Expand Down Expand Up @@ -3769,7 +3811,56 @@ export type ServerChatCompletionsInput = {
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["user"]
readonly redrob_context?: {
readonly model: string
readonly messages: ReadonlyArray<{
readonly role: "system" | "developer" | "user" | "assistant" | "tool"
readonly content?: string | null | ReadonlyArray<{ readonly [x: string]: unknown }> | undefined
readonly tool_calls?:
| ReadonlyArray<{
readonly id: string
readonly type: "function"
readonly function: { readonly name: string; readonly arguments: string }
}>
| undefined
readonly tool_call_id?: string | undefined
readonly name?: string | undefined
}>
readonly tools?:
| ReadonlyArray<{
readonly type: "function"
readonly function: {
readonly name: string
readonly description?: string | undefined
readonly parameters?: { readonly [x: string]: unknown } | undefined
}
}>
| undefined
readonly tool_choice?:
| "auto"
| "none"
| "required"
| { readonly type: "function"; readonly function: { readonly name: string } }
| undefined
readonly stream?: boolean | undefined
readonly max_tokens?: number | undefined
readonly max_completion_tokens?: number | undefined
readonly temperature?: number | undefined
readonly top_p?: number | undefined
readonly stop?: string | ReadonlyArray<string> | undefined
readonly seed?: number | undefined
readonly frequency_penalty?: number | undefined
readonly presence_penalty?: number | undefined
readonly reasoning_effort?: string | undefined
readonly user?: string | undefined
readonly redrob_context?:
| { readonly page_url?: string | undefined; readonly page_title?: string | undefined }
| undefined
}["redrob_context"]
}

export type ServerChatCompletionsOutput = {
Expand Down
54 changes: 53 additions & 1 deletion packages/core/src/plugin/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import customizeRedrobContent from "./skill/customize-redrob.md" with { type: "t
import documentToolchainContent from "./skill/document-toolchain.md" with { type: "text" }
import pageControlContent from "./skill/page-control.md" with { type: "text" }
import skillWriterContent from "./skill/skill-writer.md" with { type: "text" }
import browserGmail from "./skill/browser/gmail/SKILL.md" with { type: "text" }
import browserGoogleCalendar from "./skill/browser/google-calendar/SKILL.md" with { type: "text" }
import browserNotion from "./skill/browser/notion/SKILL.md" with { type: "text" }
import browserGithub from "./skill/browser/github/SKILL.md" with { type: "text" }
import browserSlack from "./skill/browser/slack/SKILL.md" with { type: "text" }
import browserNaverMail from "./skill/browser/naver-mail/SKILL.md" with { type: "text" }
import browserNaverSearch from "./skill/browser/naver-search/SKILL.md" with { type: "text" }
import browserKakao from "./skill/browser/kakao/SKILL.md" with { type: "text" }
import browserHometax from "./skill/browser/hometax/SKILL.md" with { type: "text" }
import browserCoupang from "./skill/browser/coupang/SKILL.md" with { type: "text" }
import { ConfigMarkdown } from "../config/markdown"
import { SkillArming } from "../skill/arming"

const mutable = <T>(value: T) => value as DeepMutable<T>

Expand Down Expand Up @@ -79,11 +91,51 @@ export const BuiltinSkills: ReadonlyArray<SkillV2.Info> = [
}),
]

/**
* CAP-1: the browser site skills. Each one is a real SKILL.md under `./skill/browser/<site>/`,
* frontmatter included, so the file a person edits is the whole truth: its `name`,
* `description`, `urls:` and `keywords:` are read from it here, through the same arming merge
* the disk loader uses. A document whose frontmatter does not parse, or has no name, is skipped
* rather than shipped as a nameless skill.
*/
const BrowserSkillDocuments: ReadonlyArray<readonly [string, string]> = [
["gmail", browserGmail],
["google-calendar", browserGoogleCalendar],
["notion", browserNotion],
["github", browserGithub],
["slack", browserSlack],
["naver-mail", browserNaverMail],
["naver-search", browserNaverSearch],
["kakao", browserKakao],
["hometax", browserHometax],
["coupang", browserCoupang],
]

export const BrowserSkills: ReadonlyArray<SkillV2.Info> = BrowserSkillDocuments.flatMap(([site, document]) => {
const markdown = ConfigMarkdown.parseOption(document)
const data = markdown?.data as Record<string, unknown> | undefined
const name = typeof data?.["name"] === "string" ? (data["name"] as string) : undefined
if (!markdown || !name) return []
const autoInject = SkillArming.fromFrontmatter(data, undefined)
return [
SkillV2.Info.make({
name,
description: typeof data?.["description"] === "string" ? (data["description"] as string) : undefined,
location: AbsolutePath.make(`/builtin/browser/${site}/SKILL.md`),
...(autoInject ? { autoInject: SkillV2.AutoInject.make(autoInject) } : {}),
content: markdown.content,
}),
]
})

/** Everything the engine registers: the core built-ins, then the browser site skills. */
export const AllBuiltinSkills: ReadonlyArray<SkillV2.Info> = [...BuiltinSkills, ...BrowserSkills]

export const Plugin = define({
id: "skill",
effect: Effect.fn(function* (ctx) {
yield* ctx.skill.transform((draft) => {
for (const skill of BuiltinSkills) {
for (const skill of AllBuiltinSkills) {
// The plugin API is typed from the generated SDK, which has no readonly arrays.
draft.source(mutable(SkillV2.EmbeddedSource.make({ type: "embedded", skill })))
}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/plugin/skill/browser/coupang/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: browser-coupang
description: 쿠팡 주문 내역과 배송 상태를 조회할 때 쓴다. 주문, 결제, 취소, 반품은 하지 않는다.
urls:
- https://www.coupang.com/*
- https://mc.coupang.com/*
keywords:
- 쿠팡
- coupang
- 주문 내역
- 배송 조회
---

# 쿠팡 주문 조회

도구: `open_page`, `read_page`, `click`, 그리고 제공될 때 `login`.

1. `open_page`로 주문 목록 `https://mc.coupang.com/ssr/desktop/order/list`를 연다. 로그인 화면이면
`login`(있을 때)을 쓰거나 사용자에게 로그인을 부탁하고 멈춘다.
2. `read_page`는 주문마다 주문일, 상품명, 금액, 배송 상태를 준다. 오래된 주문은 화면의 연도 탭을
`click`해서 넘긴다.
3. 배송 상세는 해당 주문의 "배송조회"를 `click`한 뒤 `read_page`로 택배사, 운송장 번호, 현재 위치를
읽는다.
4. 답에는 주문일, 상품명, 상태를 표처럼 정리한다.

"구매하기", "결제", "주문취소", "반품신청", "교환신청"은 누르지 않는다. 사용자가 원하면 해당 화면을
열어 두고 직접 누르도록 안내한다.
25 changes: 25 additions & 0 deletions packages/core/src/plugin/skill/browser/github/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: browser-github
description: Use when the task is reading issues, pull requests, releases or code on github.com in the browser.
urls:
- https://github.com/*
keywords:
- github
- pull request
- github issue
---

# GitHub in the browser

Tools: `open_page`, `read_page`, `click`, `type_text`, and `login` when it is offered.

1. Go straight to the page by URL instead of navigating:
- issues: `https://github.com/<owner>/<repo>/issues?q=is:open+<words>`
- pull requests: `https://github.com/<owner>/<repo>/pulls?q=is:open+author:<user>`
- one item: `https://github.com/<owner>/<repo>/pull/<n>` (add `/files` for the diff)
- releases: `https://github.com/<owner>/<repo>/releases`
- search code: `https://github.com/search?q=<words>+repo:<owner>/<repo>&type=code`
2. `read_page` gives the title, state, labels and the conversation. For a long diff, read the
files tab one file at a time.
3. Writing a comment: `click` the comment box, `type_text`, and stop before "Comment" unless the
user asked you to post. Never merge, close, approve or push anything from the page.
Loading
Loading