@agentshouse/skills 0.1.0-ce95c9bef1e33 → 0.1.0-cf6afb1bdfcaf
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 +7 -5
- package/agent-instructions.json +12 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -12
- package/guide/{routines → agent-functions}/work-on-ticket.md +6 -8
- package/guide/apps/build.md +2 -1
- package/guide/apps/chat.md +37 -8
- package/guide/apps/client.md +1 -1
- package/guide/apps/content-safety.md +8 -5
- package/guide/apps/data.md +10 -6
- package/guide/apps/design-functions.md +10 -0
- package/guide/apps/design-load.md +14 -0
- package/guide/apps/design-look.md +11 -0
- package/guide/apps/design-schema.md +13 -0
- package/guide/apps/design.md +12 -0
- package/guide/apps/example-function.md +4 -5
- package/guide/apps/example-tasks-2.md +21 -23
- package/guide/apps/example-tasks-3.md +2 -2
- package/guide/apps/example-tasks.md +8 -40
- package/guide/apps/function-times.md +10 -0
- package/guide/apps/functions.md +7 -7
- package/guide/apps/local-first.md +11 -10
- package/guide/apps/local-query-results.md +13 -4
- package/guide/apps/local-query.md +22 -14
- package/guide/apps/page.md +4 -4
- package/guide/apps/query-house.md +48 -0
- package/guide/apps/room-drafts.md +20 -13
- package/guide/apps/runtime-api.md +20 -12
- package/guide/apps/runtime-conversations.md +38 -0
- package/guide/apps/runtime-files.md +8 -6
- package/guide/apps/runtime-refusals.md +13 -6
- package/guide/apps/runtime.md +2 -1
- package/guide/apps/search.md +21 -0
- package/guide/apps/start.md +8 -3
- package/guide/apps/verify.md +4 -5
- package/guide/get-started.md +2 -4
- package/guide/index.md +5 -5
- package/guide/owner-rules.md +8 -9
- package/guide/reference/access-level.md +1 -1
- package/guide/reference/agent-function.md +15 -0
- package/guide/reference/app-manifest.md +3 -1
- package/guide/reference/group-post.md +7 -0
- package/guide/reference/inbox-types.md +7 -0
- package/guide/reference/schema.md +22 -11
- package/guide/reference/telegram-link.md +11 -0
- package/guide/reference/triggers.md +12 -4
- package/guide/reference/wayfinding-types.md +7 -0
- package/guide/sites/build.md +11 -8
- package/guide/sites/counter.md +32 -0
- package/guide/sites/entry-data-contract.md +11 -2
- package/guide/sites/first-load.md +16 -0
- package/guide/sites/functions.md +13 -0
- package/guide/sites/payments-stripe-2.md +78 -0
- package/guide/sites/payments-stripe-3.md +68 -0
- package/guide/sites/payments-stripe-4.md +96 -0
- package/guide/sites/payments-stripe.md +63 -0
- package/guide/sites/payments.md +13 -0
- package/guide/sites/runtime.md +12 -2
- package/guide/sites/tags.md +9 -0
- package/guide/skills/build-app.md +18 -7
- package/guide/skills/house-onboarding.md +8 -12
- package/guide/skills/prototype-app.md +14 -25
- package/guide/skills/wizard.md +7 -1
- package/owner-rules-earlier.json +6 -1
- package/package.json +1 -1
- package/set/code-review/SKILL.md +1 -1
- package/set/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/set/grilling/SKILL.md +1 -1
- package/set/implement-spec/SKILL.md +4 -2
- package/set/improve-codebase-architecture/SKILL.md +1 -1
- package/set/research/SKILL.md +1 -1
- package/set/wayfinder/SKILL.md +1 -1
- package/upstream.json +2 -2
- package/guide/apps/local-search.md +0 -35
- package/guide/reference/routine.md +0 -14
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@ The Skills agents.house serves its agents, and the texts its agents read.
|
|
|
11
11
|
an App, `sites/` what a Site adds, and `skills/` agents.house's own Skills, `house-onboarding`,
|
|
12
12
|
`prototype-app`, `build-app` and `wizard`, each page carrying the `name` and `description` a
|
|
13
13
|
`SKILL.md` carries.
|
|
14
|
-
`
|
|
14
|
+
`agent-functions/` holds the Agent functions agents.house writes into every Private Room, frontmatter and
|
|
15
15
|
all. `owner-rules` is the Owner rules' text, and `owner-rules-earlier.json` the SHA-256 of every
|
|
16
16
|
earlier text.
|
|
17
|
-
- `agent-instructions.json` is the lines that open an agent's first prompt
|
|
17
|
+
- `agent-instructions.json` is the lines that open an agent's first prompt; its `routing` lines are
|
|
18
|
+
the ones agents.house's MCP server instructions carry too.
|
|
18
19
|
|
|
19
20
|
```ts
|
|
20
21
|
import { OWNER_RULES, listSkills, readSkill } from '@agentshouse/skills';
|
|
@@ -23,9 +24,10 @@ for (const { name } of listSkills()) console.log(name, readSkill(name).length);
|
|
|
23
24
|
console.log(OWNER_RULES);
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
`agentInstructions(
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
`agentInstructions()` returns the first prompt's lines, which name no Local copy, `ROUTING_INSTRUCTIONS`
|
|
28
|
+
its routing lines,
|
|
29
|
+
`GUIDE` every Guide page with its path, title, first line and text and
|
|
30
|
+
`EARLIER_OWNER_RULES_SHA256` the earlier texts' digests.
|
|
29
31
|
|
|
30
32
|
The skills come from [mattpocock/skills](https://github.com/mattpocock/skills); thank you, Matt
|
|
31
33
|
Pocock.
|
package/agent-instructions.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"opening": [
|
|
3
3
|
"From House:",
|
|
4
|
-
"Work with House through the `house` CLI
|
|
5
|
-
"To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow the Guide from `index` with `house open_guide`. Never start a server.",
|
|
6
|
-
"Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.",
|
|
7
|
-
"For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller."
|
|
4
|
+
"Work with House through the `house` CLI, `house <tool> '<arguments as JSON>'`: `house --help` lists the Tools and `house help <tool>` names a Tool's arguments."
|
|
8
5
|
],
|
|
9
|
-
"
|
|
6
|
+
"routing": [
|
|
7
|
+
"Read with `shell` and `info`. Write with `files_batch`; `records_append` adds a record from one of your Sources.",
|
|
8
|
+
"For anything else, `find_command`, then `run_command`.",
|
|
9
|
+
"Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`."
|
|
10
|
+
],
|
|
11
|
+
"closing": [
|
|
12
|
+
"`house run_command '{\"command\":\"guide/open\"}'` opens `index`, the root page of House's guides and formats.",
|
|
13
|
+
"A file reaches the User as a Room Path written with `house upload` and linked in the answer. House serves an App, a Site or a website built by the Guide from `index` (`guide/open`), with no server of your own.",
|
|
14
|
+
"For another Agent's help, `agent-functions/send` naming one of the Room's Agent function cards under `house/agent-functions/`, or the Agent and what you ask, as its Standing answers there allow; its mail comes back into this conversation."
|
|
15
|
+
]
|
|
10
16
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export interface GuidePage {
|
|
|
15
15
|
export declare const GUIDE_ROOT = "index";
|
|
16
16
|
export declare const GUIDE: readonly GuidePage[];
|
|
17
17
|
export declare const OWNER_RULES: string;
|
|
18
|
-
export declare const WORK_ON_TICKET: string;
|
|
19
18
|
export declare const EARLIER_OWNER_RULES_SHA256: readonly string[];
|
|
20
|
-
export declare
|
|
19
|
+
export declare const ROUTING_INSTRUCTIONS: readonly string[];
|
|
20
|
+
export declare function agentInstructions(): string[];
|
|
21
21
|
export declare function listSkills(): readonly Skill[];
|
|
22
22
|
export declare function readSkill(name: string): readonly SkillFile[];
|
package/dist/index.js
CHANGED
|
@@ -4,20 +4,20 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
const PACKAGE = fileURLToPath(new URL('..', import.meta.url));
|
|
5
5
|
const SET = join(PACKAGE, 'set');
|
|
6
6
|
const ENTRY = 'SKILL.md';
|
|
7
|
-
const REF_PLACEHOLDER = '<house-ref>';
|
|
8
7
|
const TREE = join(PACKAGE, 'guide');
|
|
9
8
|
const PAGE = '.md';
|
|
10
9
|
const FRONTMATTER = /^---\n[\s\S]*?\n---\n\n?/u;
|
|
11
10
|
const HEADING = /^# /u;
|
|
11
|
+
const LIST_ITEM = /^\s*[-*+] /u;
|
|
12
12
|
export const GUIDE_ROOT = 'index';
|
|
13
13
|
const OWNER_RULES_PAGE = 'owner-rules';
|
|
14
|
-
const WORK_ON_TICKET_PAGE = 'routines/work-on-ticket';
|
|
15
14
|
function packaged(name) {
|
|
16
15
|
return readFileSync(join(PACKAGE, name), 'utf8');
|
|
17
16
|
}
|
|
18
17
|
function guidePage(file) {
|
|
19
18
|
const text = readFileSync(join(TREE, file), 'utf8');
|
|
20
|
-
const [heading = '', ,
|
|
19
|
+
const [heading = '', , ...rest] = text.replace(FRONTMATTER, '').split('\n');
|
|
20
|
+
const line = rest.find((held) => held !== '' && !LIST_ITEM.test(held)) ?? '';
|
|
21
21
|
return {
|
|
22
22
|
path: file.slice(0, -PAGE.length).split(sep).join('/'),
|
|
23
23
|
title: heading.replace(HEADING, ''),
|
|
@@ -33,17 +33,11 @@ export const GUIDE = readdirSync(TREE, {
|
|
|
33
33
|
.map((entry) => guidePage(relative(TREE, join(entry.parentPath, entry.name))))
|
|
34
34
|
.sort((left, right) => (left.path < right.path ? -1 : 1));
|
|
35
35
|
export const OWNER_RULES = GUIDE.find((page) => page.path === OWNER_RULES_PAGE).text;
|
|
36
|
-
export const WORK_ON_TICKET = GUIDE.find((page) => page.path === WORK_ON_TICKET_PAGE).text;
|
|
37
36
|
export const EARLIER_OWNER_RULES_SHA256 = JSON.parse(packaged('owner-rules-earlier.json'));
|
|
38
37
|
const INSTRUCTIONS = JSON.parse(packaged('agent-instructions.json'));
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return [
|
|
43
|
-
...INSTRUCTIONS.lines,
|
|
44
|
-
INSTRUCTIONS.copies.replaceAll(REF_PLACEHOLDER, houseRef),
|
|
45
|
-
...copies,
|
|
46
|
-
];
|
|
38
|
+
export const ROUTING_INSTRUCTIONS = INSTRUCTIONS.routing;
|
|
39
|
+
export function agentInstructions() {
|
|
40
|
+
return [...INSTRUCTIONS.opening, ...INSTRUCTIONS.routing, ...INSTRUCTIONS.closing];
|
|
47
41
|
}
|
|
48
42
|
export function listSkills() {
|
|
49
43
|
return readdirSync(SET, { withFileTypes: true })
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Work the Wayfinding ticket this request names, from its Question to its Answer.
|
|
3
|
-
input: The ticket to work on.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# Work on ticket
|
|
7
2
|
|
|
8
|
-
|
|
3
|
+
- **Description**: Work the Wayfinding ticket this request names, from its Question to its Answer.
|
|
4
|
+
- **Input**: The ticket to work on.
|
|
5
|
+
|
|
6
|
+
The Agent function an Agent runs when a person sets a Wayfinding ticket in progress with that Agent as its assignee.
|
|
9
7
|
|
|
10
8
|
Work the one ticket this request names, then stop:
|
|
11
9
|
|
|
12
|
-
1. Open the `wayfinding-ticket` the request names and set its `conversation` to this conversation
|
|
10
|
+
1. Open the `wayfinding-ticket` the request names and set its `conversation` to the id of this conversation, the one `conversations/active/list` marks `this`.
|
|
13
11
|
2. Work it by its `kind`: for `research`, find out and say where you found it; for `prototype`, build the smallest thing that answers its Question; for `grilling`, ask your User until its Question is settled; for `task`, do what it asks.
|
|
14
|
-
3. Write what you found or made
|
|
12
|
+
3. Write what you found or made as its Answer and set its `status` to `done`.
|
|
15
13
|
4. Start no other ticket, not even one this ticket unblocks.
|
package/guide/apps/build.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
How to build an App over your User's Rooms and publish it, one page per step.
|
|
4
4
|
|
|
5
|
-
An App is a web page over your User's Rooms, for your User and whom they share it with (`
|
|
5
|
+
An App is a web page over your User's Rooms, for your User and whom they share it with (`shares/create`). House serves the built files and gives the page the App runtime, which holds every bound Room but a Search-level one in the browser and syncs it with House. There is no server, backend or House API of yours to call.
|
|
6
6
|
|
|
7
|
+
- [Design an App](apps/design)
|
|
7
8
|
- [Start an App](apps/start)
|
|
8
9
|
- [An App's data](apps/data)
|
|
9
10
|
- [Local first](apps/local-first)
|
package/guide/apps/chat.md
CHANGED
|
@@ -8,16 +8,45 @@ no dependency. `const chat = await houseBridge.chat.mount(element, options)`, on
|
|
|
8
8
|
object House hands the page as `window.houseBridge`, which `houseBridge()` of
|
|
9
9
|
`@agentshouse/app-bridge` returns, draws it in `element` over the App's own client and
|
|
10
10
|
resolves to a handle: `chat.update(options)` replaces its options and `chat.unmount()`
|
|
11
|
-
removes it.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
removes it. It fills the element and lays itself out by the width it gives: the list of the
|
|
12
|
+
User's Agents and their chats or one chat on a narrow element, and on a wider one the list
|
|
13
|
+
beside the chat while the User shows it by its toggle, closed at first beside an open chat and
|
|
14
|
+
open where none is, the conversation keeping its width where the element has room for both.
|
|
15
|
+
With no chat open it shows what
|
|
16
|
+
asks the User and what works now. `conversation` opens one chat, and with it `null` the Chat
|
|
17
|
+
opens a new chat with `agent`, the Agent it goes to, or the list with neither. `onOpen` hears
|
|
18
|
+
each chat it opens as `{ conversation, agent }`, and both `null` when the User goes back to the
|
|
19
|
+
list, and `onLink` takes a link without a scheme in place of the browser.
|
|
20
|
+
Or mount it as the Chat button, which takes no element:
|
|
21
|
+
`const button = await houseBridge.chat.mountButton(options)` draws House's dot at the
|
|
22
|
+
window's bottom-right corner, which counts what asks the User or, while nothing does, the chats
|
|
23
|
+
that replied since the User opened them, and breathes while a turn runs; pressing it opens the
|
|
24
|
+
Chat in a sheet above the page, and a second press hides it. With `agent` among the options
|
|
25
|
+
the dot's first press opens it on a new chat with that Agent instead of a chat kept from an
|
|
26
|
+
earlier visit, and later presses reopen the chat it last showed. The dot stays in view over the
|
|
27
|
+
sheet, which the User moves, resizes up to the window's edges and lets fill the window, and
|
|
28
|
+
whose list, closed at first, widens it to the left by the list's own width; the page under it
|
|
29
|
+
never changes, so leave nothing of the page to make room for it.
|
|
30
|
+
`button.open(target)` opens the sheet, on `{ conversation }` or on a new chat with
|
|
31
|
+
`{ agent }`, or as it was with no target, and with `docked: true` docked at full height along
|
|
32
|
+
the window's right edge; `button.hide()` hides it, keeping its chat, scroll and typed text;
|
|
33
|
+
`button.update(options)` and `button.unmount()` work as for a page, with `onOpen` and
|
|
34
|
+
`onLink`, though a Room document's link opens House in a new tab with that chat. To open on text written in, write it as the chat's draft through the runtime's
|
|
35
|
+
`chatDraft` first, keyed `new:<agent>` for a new chat. In the sheet the input's + holds This
|
|
36
|
+
page, which attaches the page's Page metadata to one message: a new chat's first message starts
|
|
37
|
+
with it ticked. Mount one button per page.
|
|
38
|
+
In either form the Chat draws inside its own shadow root, so no rule of the App's stylesheet
|
|
39
|
+
reaches it. `chat.mount` needs an element that can hold a shadow root, such as a `div` or a
|
|
40
|
+
`section`, and rejects on a `li`, a `td` or a `form`.
|
|
16
41
|
Theme its own styles with CSS custom properties on the element or an ancestor:
|
|
17
42
|
`--chat-background`, `--chat-surface`, `--chat-text`, `--chat-text-muted`,
|
|
18
43
|
`--chat-accent`, `--chat-line`, `--chat-attention`, `--chat-danger`, `--chat-font`,
|
|
19
44
|
`--chat-font-mono`, `--chat-font-size`, `--chat-radius` and `--chat-density` (a factor for
|
|
20
|
-
spacing); an unset one keeps House's light or dark look.
|
|
21
|
-
the
|
|
45
|
+
spacing); an unset one keeps House's light or dark look. For the Chat button, which takes no
|
|
46
|
+
element, set them on the document's root element. Its dot keeps House's colours and type, and
|
|
47
|
+
stays level with the sheet's input.
|
|
48
|
+
It shows conversations only where the release declares them;
|
|
49
|
+
otherwise it lists no conversation and no Agent. A secret
|
|
22
50
|
question is never answered here; typed answers live in the Presentation State members named
|
|
23
|
-
`typed
|
|
51
|
+
`typed:…`, and the sheet's size, place, whether it fills the window and its open chat in
|
|
52
|
+
`chat:sheet`: leave them.
|
package/guide/apps/client.md
CHANGED
|
@@ -18,4 +18,4 @@ and returns `{ state: 'loading' }` until the topic's first value arrives, then `
|
|
|
18
18
|
parts)`, which saves through `saveParts` and rejects with a recorded failure.
|
|
19
19
|
`saveWritten(room, roomDrafts)` saves Room Drafts the App wrote, each named with the
|
|
20
20
|
revision its last write returned: one another tab wrote since is refused as `stale`, and
|
|
21
|
-
nothing is saved.
|
|
21
|
+
nothing is saved.
|
|
@@ -10,9 +10,12 @@ images and other resources only from origins the Provider's policy allows. An ag
|
|
|
10
10
|
never read the authorization, load code from a Room, call the Provider directly, or rebase
|
|
11
11
|
a refused Change Batch.
|
|
12
12
|
|
|
13
|
-
MD UI uses semantic HTML, is fully operable from a keyboard with
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
MD UI uses semantic HTML, is fully operable from a keyboard with labelled controls, styles
|
|
14
|
+
no focus, removes the browser's own focus ring and takes no focus by itself, and lays out
|
|
15
|
+
from a 360 pixel wide viewport to a desktop.
|
|
16
|
+
Its strings are English only, dates and numbers follow the browser locale, and content is
|
|
17
|
+
never translated. Light and dark themes come from CSS custom properties, defaulting to the
|
|
17
18
|
browser's preference and respecting an App's override. What an agent adds carries none of
|
|
18
|
-
these promises by itself.
|
|
19
|
+
these promises by itself. The App keeps no data of its own anywhere: its data lives in
|
|
20
|
+
its Rooms, at the Provider and on every device that holds them, and the
|
|
21
|
+
App's code sends it out only to the external origins its manifest declares.
|
package/guide/apps/data.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
How an App keeps what it shows as typed documents in the Rooms bound to it.
|
|
4
4
|
|
|
5
|
-
- Design the Room schema first. Each thing the App shows is one Markdown document in a Room: its frontmatter `type` names its MD type
|
|
6
|
-
- Keep each type in its own folder, such as `tasks/`, and read it by `pathPrefix
|
|
5
|
+
- Design the Room schema first. Each thing the App shows is one Markdown document in a Room: its frontmatter `type` names its MD type and its body writes the values: its `name` as the `#` line, each short value as a `- **Label**: value` line and long text under its own heading ([Schema](reference/schema)). `schema.yaml` declares the types ([Schema](reference/schema)).
|
|
6
|
+
- Keep each type in its own folder, such as `tasks/`, and read it by `pathPrefix`. A query naming `types` fails with `unknown-type` over bound Rooms none of which declares them, and answers `{ pending }` instead while the App reaches no bound Room yet; an observed query that has already answered answers `{ pending }` while its bound Rooms refuse it, and answers again once they declare what it names.
|
|
7
7
|
- Link types with `reference` fields instead of copying values; a query follows them with `hop` and `backlinks`.
|
|
8
|
-
- `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses ([App manifest](reference/app-manifest)). Bind Rooms with `
|
|
9
|
-
- A bound Room must hold each slot type identically in its own `settings/schema.yaml`, beside any other types, or
|
|
10
|
-
-
|
|
11
|
-
-
|
|
8
|
+
- `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses ([App manifest](reference/app-manifest)). Bind Rooms with `apps/bindings/set`; App code cannot bind and reaches only bound Rooms, so it opens `bridge.ceremony('/bind/<app_ref>?slot=<name>')`, the House page where your User binds Rooms, which returns to the App.
|
|
9
|
+
- A bound Room must hold each slot type identically in its own `settings/schema.yaml`, beside any other types, or its binding warns the User who bound it, and `apps/bindings/list` names that release and slot in its `mismatch` line, to the App's owner too. Write it with `files_batch` before binding; `rooms/create` makes a new Room.
|
|
10
|
+
- A slot may hold a Space instead (`apps/bindings/set` with `space_ref`): every Room of it your User holds, now and later. An App its owner puts into a Space (`apps/space/set`) uses that Space's schema instead of its own `schema.yaml`, and every member of the Space uses it, each slot holding the Space's Rooms.
|
|
11
|
+
- Read an existing Room's `settings/schema.yaml` before inventing types. Two Apps over one schema are two interfaces over the same data, and your User's agents read and write those documents too.
|
|
12
|
+
- House's own facts, such as your User's Rooms and the Space each is in, come from House acts: name each in `app.json` `house_acts` and call it with `runtime.call('<act>', input)`, such as `runtime.call('rooms/list', {})`. A call needs a connection, so offline show what the bound Rooms hold. A slot holding the Private Room brings its Triggers and none of the other documents House writes under its `house/`.
|
|
13
|
+
- `shares/create` lets a person into Rooms and Apps: its answer's `added` is `true` when their _Who can add me_ setting admits your User, so they are in at once, and `false` when the Share waits for their Accept.
|
|
14
|
+
- What no browser can do, such as acting when documents change or at a set time, is a Trigger (`triggers/create`) that notifies or runs one of the App's [functions](apps/functions) on House's servers; what it writes reaches the App at once.
|
|
15
|
+
- A Trigger may also start an [Agent function](reference/agent-function) on an Agent of your User.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Which function does what
|
|
2
|
+
|
|
3
|
+
Which part of House does each kind of an App's work, and what each run costs.
|
|
4
|
+
|
|
5
|
+
- **The page** reads and writes the bound Rooms on the device, offline too, and runs only while someone has it open ([Local first](apps/local-first)).
|
|
6
|
+
- **An App function** runs on House's servers when a Trigger of its Room fires, with no page open and no model. It reads and writes that Room and calls the services `external_origins` lists, within <function-time> and <function-memory> a run. It does what a script can: computing, filing, copying, counting, scheduling, calling a service ([App functions](apps/functions)).
|
|
7
|
+
- **An Environment function** is a script your User kept for one of their computers, which Kit runs there as them. It does work that needs that computer, its files or its logins. A Trigger of their Private Room, their Run now or `environments/functions/run` starts one; a Trigger of any other Room never does ([wizard](skills/wizard)).
|
|
8
|
+
- **An Agent function** starts a conversation of one of your User's Agents, which reads, judges and writes in minutes rather than milliseconds. Every run spends your User's own provider budget, and House sets no allowance on these runs: one Trigger acts on at most <trigger-events-per-hour> Events in a UTC hour, so a Room's Triggers together can still start many. It does what needs judgment: reading a message, answering it, deciding what a document means ([Agent function](reference/agent-function)).
|
|
9
|
+
- **A job that needs judgment in one step** splits: an App function does the rest, and an Agent function only that step, writing its answer into the Room, where a Trigger hands it on to the App function ([Agent function](reference/agent-function)).
|
|
10
|
+
- **With no page open**, nothing of the App runs by itself: a Trigger whose action notifies someone or runs one of these starts its work ([Triggers](reference/triggers)).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Designs that load House
|
|
2
|
+
|
|
3
|
+
What an App's saves, Triggers and functions cost House's servers and every device holding its Rooms.
|
|
4
|
+
|
|
5
|
+
- **A save** stores the document's whole new text as a revision, tests it against every Trigger of its Room inside the write, streams the whole text to every device holding the Room and counts once against the saver's Write limit, whatever its size. A log or a state kept in one growing document costs its whole size on every change: keep one small document per thing.
|
|
6
|
+
- **Saving as someone types**, or on a timer, writes each time; the Room Draft keeps what they type until they save ([Room Drafts](apps/room-drafts)).
|
|
7
|
+
- **Every device** holding a Room at the Keep or Open level holds all of its text ([Access level](reference/access-level)), so records written by the thousand fill every holder's device; a file goes in as a file ([files](apps/runtime-files)), never as text in a document.
|
|
8
|
+
- **A Trigger** whose change test is any edit, `changes: "text"`, acts on every save of a document it matches, while a field's change, or the default firing as a document starts to match, acts once per change that matters ([Triggers](reference/triggers)).
|
|
9
|
+
- **A function's writes** fire Triggers, its own included: one whose write matches its own Trigger runs again on each run until that Trigger's <trigger-events-per-hour> Events in the hour are spent, so write what it writes so that its Trigger does not match it ([App functions](apps/functions)).
|
|
10
|
+
- **A function run** reads, on every Event, every document of the types, folders and paths its query names from House's database, within <function-memory>, and one Room owner's runs take at most <function-owner-slots> slots on a server at once: name the narrowest folder or paths, or read a summary document the function keeps ([App functions](apps/functions)).
|
|
11
|
+
- **One Trigger's Events** run one after another, so a busy Trigger builds a queue ([App functions](apps/functions)).
|
|
12
|
+
- **An Agent function run** spends your User's provider budget each time ([Agent function](reference/agent-function)).
|
|
13
|
+
- **Changing a field's `title`** relabels every document of its type in one write ([Schema](reference/schema)).
|
|
14
|
+
- **A query naming no `types`, `pathPrefix` or `paths`** reads every document of its Rooms on the device: name the narrowest and select only what a screen shows ([Local Query](apps/local-query)).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# An App's look
|
|
2
|
+
|
|
3
|
+
What House draws around an App's page, and what the App sets for its look.
|
|
4
|
+
|
|
5
|
+
- **The page is the App's own**, drawn and run by the App ([An App's page](apps/page)). Over it House draws only its wait until the page shows its place, and the dialog asking a User who does not own the App to allow a browser feature or House act the App uses for the first time.
|
|
6
|
+
- **The Chat**, where the page mounts it, draws in the App's frame: the Chat button's dot in House's look, and the Chat in its own, which the `--chat-*` properties theme ([The Chat](apps/chat)).
|
|
7
|
+
- **The design system's tokens**: mdui's `style.css` holds House's design tokens as CSS custom properties, light and dark: its greys, type sizes, spacing, radii and motion. An App built on them looks like House; changing them gives it a look of its own. The design system itself is not published, so mdui's tokens are how an App takes it.
|
|
8
|
+
- **Light and dark**: House's screens around the page, such as its wait until the page shows its place, follow the device's light or dark setting, as `prefers-color-scheme` does.
|
|
9
|
+
- **The top colour**: declare the page's top colour as `theme_colour` in `app.json`, and `theme_colour_dark` where the dark one differs ([App manifest](reference/app-manifest)). An installed App's title bar and a phone's top strip take it, and House's white or near-black ground without it. A page with its own light and dark switch calls `bridge.setTab({ scheme })` with `'light'` or `'dark'` when it shows one over the device's setting, and `null` when it follows the device, so they take that scheme's colour.
|
|
10
|
+
- **The icon** is the owner's setting, never the release's: one image in the browser tab, the panel and on a home screen. It is the App cube in the App's colour until one is set, then a generated one from one to four colours or the owner's own square PNG or SVG, through `apps/tab-icon/set`; `bridge.setTab({ title, icon })` changes the open tab's title and icon until the next load.
|
|
11
|
+
- **No focus mark**: House's own screens draw none, and [An App's page](apps/page) says how a page removes the browser's ring.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Schemas that read well
|
|
2
|
+
|
|
3
|
+
How an App's types make documents that read well as Markdown, to people and agents alike.
|
|
4
|
+
|
|
5
|
+
A typed document writes its values as Markdown, so its schema decides how it reads; [Schema](reference/schema) holds the format.
|
|
6
|
+
|
|
7
|
+
- A field named `name` is the document's `#` line, and a type with none writes documents with no title line.
|
|
8
|
+
- `title` on a field is the label its line shows, and a field with none shows its key.
|
|
9
|
+
- Each field takes one JSON type. A short value is one line, a yes or no a checkbox line and a `reference` a link; Markdown text (`contentMediaType: text/markdown`) and a value that may hold a line break take a heading of their own; a value of no single type is written as a fenced `json` block.
|
|
10
|
+
- Rows of short values read as a table, and a list of groups that is no table takes one heading per item, named by its `name`.
|
|
11
|
+
- Nest values at most five levels deep, each list's items counting as a level. A value that takes a heading goes one heading level deeper per level, from `##` to `######`, so a schema that places one deeper is refused `heading-depth`, and a deeper plain string that holds a line break is refused `malformed-value` when it is written.
|
|
12
|
+
- One document is one thing, such as a task, a lead or an order: link others by `reference` rather than copying or nesting them, and keep a growing list of records as a folder of documents.
|
|
13
|
+
- A type whose Room your User holds at the Search level, which the App asks through a [House query](apps/query-house), names in `indexed` the fields that query orders, totals or groups by, a list of its own top-level keys, and House keeps an index of their values. A field there holds one plain value, `null` allowed: a number, an integer, a yes or no, or a string that is a choice, a `date`, a `date-time`, a `time` or bounded by a `maxLength` of at most <indexed-text-length> characters; a list, a group, a value of no single type, Markdown text, a `reference` and a House key such as `member` or `submitted` are not. A type names at most <indexed-fields>, and House refuses a schema naming more, an unknown field or one it cannot index with the rule `indexed` and its place. Naming one changes no document and checks none again, so whoever writes the schema may change it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Design an App
|
|
2
|
+
|
|
3
|
+
What an App's design settles before its code, one page per question House answers.
|
|
4
|
+
|
|
5
|
+
- [Which function does what](apps/design-functions): which part of House does each piece of the App's work, and what it costs.
|
|
6
|
+
- [Schemas that read well](apps/design-schema): how its types make documents that people and agents read.
|
|
7
|
+
- [Designs that load House](apps/design-load): what its saves, Triggers and functions cost, and the designs that cost less.
|
|
8
|
+
- [An App's look](apps/design-look): what House draws around its page, and what the App sets.
|
|
9
|
+
|
|
10
|
+
Keep a Room for the App where you write how it is developed: its decisions, its plan and tickets, and what its Triggers and functions do. Name that Room in the App's README, so the next agent working on the App finds it. House reads nothing there.
|
|
11
|
+
|
|
12
|
+
[Example: tasks](apps/example-tasks) follows these pages.
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
How a function hands each new lead to the manager on shift with the fewest leads since their shift began, records its decision and calls the CRM.
|
|
4
4
|
|
|
5
|
-
A CRM posts each new lead to the Room's push address (`
|
|
5
|
+
A CRM posts each new lead to the Room's push address (`capture/push-addresses/create`) as `{"event": "lead.created", "card": {"id": 101}}`. Name where the CRM puts its event id as the address's `event_id`, when you create it or later with `capture/push-addresses/edit`, so a lead it sends again writes nothing; an address naming none takes a `webhook-id` header as the id. The Room holds three types: `staff/<key>.md` with `manager_id`, the manager's id at the CRM; `shifts/<key>.md` with `staff`, the key, and `starts` and `ends` as ISO instants; and `decisions/<card>.md` with `card`, `staff` and `decided_at`. The App's `external_origins` lists the CRM's origin, the Room holds a Service key for it, and a Trigger runs the function for each lead:
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
9
9
|
"on": "items",
|
|
10
|
-
"address": "<push address>",
|
|
11
|
-
"match": { "path": "/json/event", "eq": "lead.created" }
|
|
10
|
+
"query": { "where": { "/address": "<push address>", "/json/event": "lead.created" } }
|
|
12
11
|
}
|
|
13
12
|
```
|
|
14
13
|
|
|
@@ -43,7 +42,7 @@ export default async (event, house) => {
|
|
|
43
42
|
const answer = await house.callExternal({
|
|
44
43
|
url: `https://crm.example/cards/${card}`,
|
|
45
44
|
method: 'PUT',
|
|
46
|
-
headers: { 'content-type': 'application/json', 'idempotency-key': event.
|
|
45
|
+
headers: { 'content-type': 'application/json', 'idempotency-key': event.id },
|
|
47
46
|
body: JSON.stringify({ status: 'assigned', manager: manager.fields.manager_id }),
|
|
48
47
|
room_ref: event.room,
|
|
49
48
|
});
|
|
@@ -64,4 +63,4 @@ export default async (event, house) => {
|
|
|
64
63
|
};
|
|
65
64
|
```
|
|
66
65
|
|
|
67
|
-
A lead the CRM delivers again finds its decision and calls nothing. Two leads that arrive together run one after another, so the second counts the first's decision and goes to another manager when two are tied. Each count is a query's `total`, so a long shift is never cut at a page. A failed call fails the run with the CRM's answer
|
|
66
|
+
A lead the CRM delivers again finds its decision and calls nothing. Two leads that arrive together run one after another, so the second counts the first's decision and goes to another manager when two are tied. Each count is a query's `total`, so a long shift is never cut at a page. A failed call fails the run with the CRM's answer; the next lead still runs.
|
|
@@ -12,8 +12,8 @@ function taskCreation(draft: Draft) {
|
|
|
12
12
|
: undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
return draft?.parts.fields?.
|
|
15
|
+
function nameOf(draft: Draft): unknown {
|
|
16
|
+
return draft?.parts.fields?.name?.value ?? taskCreation(draft)?.fields.name;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function el<K extends keyof HTMLElementTagNameMap>(
|
|
@@ -26,31 +26,29 @@ function el<K extends keyof HTMLElementTagNameMap>(
|
|
|
26
26
|
return node;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function nameForm(
|
|
30
30
|
action: string,
|
|
31
31
|
room: string,
|
|
32
32
|
path: () => string,
|
|
33
33
|
shown: unknown,
|
|
34
|
-
parts: (
|
|
35
|
-
done: (
|
|
34
|
+
parts: (name: string, held: Draft) => Parts,
|
|
35
|
+
done: (name: HTMLInputElement) => void,
|
|
36
36
|
) {
|
|
37
37
|
let held = drafts.current().find((draft) => draft.room === room && draft.path === path()) ?? null;
|
|
38
|
-
const
|
|
39
|
-
value: String(
|
|
38
|
+
const name = el('input', {
|
|
39
|
+
value: String(nameOf(held) ?? shown),
|
|
40
40
|
required: true,
|
|
41
|
-
ariaLabel: '
|
|
41
|
+
ariaLabel: 'Name',
|
|
42
42
|
});
|
|
43
43
|
const keep = () =>
|
|
44
44
|
queued(async () => {
|
|
45
|
-
|
|
46
|
-
const written = await write(room, path(), held, parts(title.value, held));
|
|
47
|
-
unsaved.delete(title);
|
|
45
|
+
const written = await write(room, path(), held, parts(name.value, held));
|
|
48
46
|
held = written.draft;
|
|
49
|
-
if (written.stale)
|
|
47
|
+
if (written.stale) name.value = String(nameOf(held) ?? shown);
|
|
50
48
|
return written.stale;
|
|
51
49
|
});
|
|
52
|
-
|
|
53
|
-
const form = el('form', {},
|
|
50
|
+
name.oninput = () => void keep();
|
|
51
|
+
const form = el('form', {}, name, el('button', {}, action));
|
|
54
52
|
form.onsubmit = async (event) => {
|
|
55
53
|
event.preventDefault();
|
|
56
54
|
const stale = await keep();
|
|
@@ -58,16 +56,16 @@ function titleForm(
|
|
|
58
56
|
if (stale) return void (note.textContent = 'Not saved: stale.');
|
|
59
57
|
if (!(await save(room, held))) return;
|
|
60
58
|
held = null;
|
|
61
|
-
done(
|
|
59
|
+
done(name);
|
|
62
60
|
};
|
|
63
61
|
return form;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
function editForm(room: string, path: string, shown: unknown) {
|
|
67
|
-
const parts = (
|
|
68
|
-
fields: {
|
|
65
|
+
const parts = (name: string): Parts => ({
|
|
66
|
+
fields: { name: { value: name, base: { value: shown } } },
|
|
69
67
|
});
|
|
70
|
-
return
|
|
68
|
+
return nameForm(
|
|
71
69
|
'Save',
|
|
72
70
|
room,
|
|
73
71
|
() => path,
|
|
@@ -78,22 +76,22 @@ function editForm(room: string, path: string, shown: unknown) {
|
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
function addForm(room: string) {
|
|
81
|
-
const parts = (
|
|
79
|
+
const parts = (name: string, held: Draft): Parts => {
|
|
82
80
|
const kept = taskCreation(held) ?? { type: 'task', fields: { done: false }, sections: {} };
|
|
83
|
-
return { creation: { ...kept, fields: { ...kept.fields,
|
|
81
|
+
return { creation: { ...kept, fields: { ...kept.fields, name } } };
|
|
84
82
|
};
|
|
85
83
|
const adding = drafts
|
|
86
84
|
.current()
|
|
87
85
|
.find((draft) => draft.room === room && draft.path.startsWith('tasks/') && taskCreation(draft));
|
|
88
86
|
let path = adding?.path ?? `tasks/${crypto.randomUUID()}.md`;
|
|
89
|
-
return
|
|
87
|
+
return nameForm(
|
|
90
88
|
'Add',
|
|
91
89
|
room,
|
|
92
90
|
() => path,
|
|
93
91
|
'',
|
|
94
92
|
parts,
|
|
95
|
-
(
|
|
96
|
-
|
|
93
|
+
(name) => {
|
|
94
|
+
name.value = '';
|
|
97
95
|
path = `tasks/${crypto.randomUUID()}.md`;
|
|
98
96
|
},
|
|
99
97
|
);
|
|
@@ -23,7 +23,7 @@ function draw(): void {
|
|
|
23
23
|
note.textContent = '';
|
|
24
24
|
if (room === undefined) main.replaceChildren('This App has no Room yet.');
|
|
25
25
|
else if (open !== undefined)
|
|
26
|
-
main.replaceChildren(editForm(open.room, open.path, open.fields!.
|
|
26
|
+
main.replaceChildren(editForm(open.room, open.path, open.fields!.name), note);
|
|
27
27
|
else main.replaceChildren(list, addForm(room.locator), note);
|
|
28
28
|
}
|
|
29
29
|
if (!('pending' in answer)) shown();
|
|
@@ -35,7 +35,7 @@ function draw(): void {
|
|
|
35
35
|
const done = task.fields!.done === true;
|
|
36
36
|
const box = el('input', { type: 'checkbox', checked: done, ariaLabel: 'Done' });
|
|
37
37
|
box.onchange = () => void queued(() => toggle(task.room, task.path, done));
|
|
38
|
-
const link = el('a', { href: task.path }, String(task.fields!.
|
|
38
|
+
const link = el('a', { href: task.path }, String(task.fields!.name));
|
|
39
39
|
link.onclick = (event) => {
|
|
40
40
|
event.preventDefault();
|
|
41
41
|
void bridge.push(`/${task.path}`);
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
A whole tasks App over one Room, keeping every rule of the App pages; its `main.ts` goes on over two more pages.
|
|
4
4
|
|
|
5
|
-
`schema.yaml`, also written into the Room's `settings/schema.yaml
|
|
5
|
+
`schema.yaml`, also written into the Room's `settings/schema.yaml`, so a task reads as its `# Buy milk` line over a `- [ ] Done` line:
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
8
|
task:
|
|
9
9
|
properties:
|
|
10
|
-
|
|
11
|
-
done: { type: boolean }
|
|
12
|
-
required: [
|
|
10
|
+
name: { type: string }
|
|
11
|
+
done: { type: boolean, title: Done }
|
|
12
|
+
required: [name, done]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`app.json`, then `
|
|
15
|
+
`app.json`, then `apps/bindings/set` with slot `tasks`:
|
|
16
16
|
|
|
17
17
|
```json
|
|
18
18
|
{ "entry": "index.html", "room_slots": [{ "name": "tasks", "access": "write", "types": ["task"] }] }
|
|
@@ -23,51 +23,19 @@ task:
|
|
|
23
23
|
```ts
|
|
24
24
|
import { houseBridge } from '@agentshouse/app-bridge';
|
|
25
25
|
|
|
26
|
-
type Task = { top: number; field: string | null; start: number | null; end: number | null };
|
|
27
|
-
|
|
28
26
|
const bridge = houseBridge();
|
|
29
27
|
let shown!: () => void;
|
|
30
28
|
const showing = new Promise<void>((resolve) => (shown = resolve));
|
|
31
29
|
let writing: Promise<unknown> = Promise.resolve();
|
|
32
|
-
|
|
33
|
-
bridge.cooperate({
|
|
34
|
-
checkpoint: () => showing,
|
|
35
|
-
async persist() {
|
|
36
|
-
await writing;
|
|
37
|
-
if ([...unsaved].some((title) => title.isConnected)) throw new Error('unsaved');
|
|
38
|
-
},
|
|
39
|
-
capture(): Task {
|
|
40
|
-
const at = document.activeElement;
|
|
41
|
-
const field = at instanceof HTMLInputElement && at.selectionStart !== null ? at : null;
|
|
42
|
-
return {
|
|
43
|
-
top: scrollY,
|
|
44
|
-
field: field?.ariaLabel ?? null,
|
|
45
|
-
start: field?.selectionStart ?? null,
|
|
46
|
-
end: field?.selectionEnd ?? null,
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
async restore(task) {
|
|
50
|
-
await showing;
|
|
51
|
-
const held = task as Task | null;
|
|
52
|
-
if (held === null) return;
|
|
53
|
-
const field = Array.from(document.querySelectorAll('input')).find(
|
|
54
|
-
(input) => held.field !== null && input.ariaLabel === held.field,
|
|
55
|
-
);
|
|
56
|
-
field?.focus({ preventScroll: true });
|
|
57
|
-
field?.setSelectionRange(held.start, held.end);
|
|
58
|
-
scrollTo(0, held.top);
|
|
59
|
-
},
|
|
60
|
-
ready: () => true,
|
|
61
|
-
settle() {},
|
|
62
|
-
});
|
|
30
|
+
bridge.cooperate({ placed: () => showing });
|
|
63
31
|
const runtime = await bridge.runtime;
|
|
64
32
|
const rooms = await runtime.observe('rooms');
|
|
65
33
|
const sync = await runtime.observe('sync-status');
|
|
66
34
|
const drafts = await runtime.observe('room-drafts');
|
|
67
35
|
const tasks = await runtime.observe('query', {
|
|
68
36
|
pathPrefix: 'tasks',
|
|
69
|
-
select: { fields: ['/
|
|
70
|
-
orderBy: [{ by: { field: '/
|
|
37
|
+
select: { fields: ['/name', '/done'] },
|
|
38
|
+
orderBy: [{ by: { field: '/name' } }],
|
|
71
39
|
});
|
|
72
40
|
const main = document.querySelector('main')!;
|
|
73
41
|
const list = document.createElement('div');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Times in an App function
|
|
2
|
+
|
|
3
|
+
How a function reads its Room's local time and turns a date and time there into a moment and back, with no time-zone library.
|
|
4
|
+
|
|
5
|
+
- **The event.** Beside `moment`, the UTC instant `Date` reads, the event carries the Room's `time_zone`, such as `Europe/Kyiv`, and the moment's `date` (`YYYY-MM-DD`) and `time` (`HH:MM:SS`) there, so the same day is the Room's day: `order.fields.delivery === event.date`.
|
|
6
|
+
- **To a moment.** `await house.toMoment({ date: '2026-11-10', time: '13:00' })` answers the ISO instant of a date and an `HH:MM` or `HH:MM:SS` time in the Room's zone, `'2026-11-10T11:00:00.000Z'` in Kyiv. At daylight saving, a time the clocks skip takes the moment just after the gap, `04:00` for `03:30` on the day Kyiv's clocks jump from `03:00` to `04:00`, and a time they repeat takes its earlier moment.
|
|
7
|
+
- **To the Room's time.** `await house.toLocal({ moment: event.moment })` answers the `time_zone`, `date` and `time` in the Room's zone of a date-time with its offset, written as a document's `date-time` field takes it.
|
|
8
|
+
- **Each is a call to House** that counts as no external call and rejects what it refuses with its `code`: `invalid-time` for what is no date and time, or no date-time with its offset, and `unknown-room` for a `room_ref` naming any Room but the run's own.
|
|
9
|
+
|
|
10
|
+
Next: [App functions](apps/functions)
|