@agentshouse/skills 0.1.0-cdf317eaa8570 → 0.1.0-ce95c9bef1e33
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/guide/apps/build.md +1 -0
- package/guide/apps/data.md +1 -1
- package/guide/apps/example-function.md +67 -0
- package/guide/apps/functions.md +13 -0
- package/guide/apps/local-first.md +2 -2
- package/guide/apps/room-drafts.md +1 -1
- package/guide/index.md +2 -2
- package/guide/reference/access-level.md +9 -0
- package/guide/reference/app-manifest.md +1 -1
- package/guide/reference/triggers.md +2 -2
- package/package.json +1 -1
- package/guide/reference/read-level.md +0 -9
package/guide/apps/build.md
CHANGED
package/guide/apps/data.md
CHANGED
|
@@ -8,4 +8,4 @@ How an App keeps what it shows as typed documents in the Rooms bound to it.
|
|
|
8
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 `set_app_bindings`; 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
9
|
- A bound Room must hold each slot type identically in its own `settings/schema.yaml`, beside any other types, or your User is warned. Write it with `edit` before binding; `create_room` makes a new Room.
|
|
10
10
|
- 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.
|
|
11
|
-
- What no browser can do, such as acting when documents change or at a set time, is a Trigger (`create_trigger`) that notifies
|
|
11
|
+
- What no browser can do, such as acting when documents change or at a set time, is a Trigger (`create_trigger`) that notifies, starts a Routine on an Agent, or runs one of the App's [functions](apps/functions) on House's servers; what it writes reaches the App at once.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Example: assign a lead
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
A CRM posts each new lead to the Room's push address (`create_push_address`) as `{"event": "lead.created", "card": {"id": 101}}`. 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
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"on": "items",
|
|
10
|
+
"address": "<push address>",
|
|
11
|
+
"match": { "path": "/json/event", "eq": "lead.created" }
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
export default async (event, house) => {
|
|
17
|
+
const lead = JSON.parse(event.item.body);
|
|
18
|
+
const now = event.moment;
|
|
19
|
+
const card = lead.card.id;
|
|
20
|
+
const read = async (query) => house.query({ select: { fields: 'all' }, ...query });
|
|
21
|
+
const [held] = (await read({ types: ['decision'], paths: [`decisions/${card}.md`] })).documents;
|
|
22
|
+
if (held !== undefined) return;
|
|
23
|
+
const { documents: shifts } = await read({
|
|
24
|
+
types: ['shift'],
|
|
25
|
+
where: { '/starts': { $lte: now }, '/ends': { $gt: now } },
|
|
26
|
+
});
|
|
27
|
+
if (shifts.length === 0) throw new Error('no manager is on shift');
|
|
28
|
+
const leads = async (shift) =>
|
|
29
|
+
(
|
|
30
|
+
await read({
|
|
31
|
+
types: ['decision'],
|
|
32
|
+
where: { '/staff': shift.fields.staff, '/decided_at': { $gte: shift.fields.starts } },
|
|
33
|
+
limit: 1,
|
|
34
|
+
total: true,
|
|
35
|
+
})
|
|
36
|
+
).total;
|
|
37
|
+
const counted = await Promise.all(
|
|
38
|
+
shifts.map(async (shift) => ({ shift, leads: await leads(shift) })),
|
|
39
|
+
);
|
|
40
|
+
const [{ shift: chosen }] = counted.sort((left, right) => left.leads - right.leads);
|
|
41
|
+
const [manager] = (await read({ types: ['staff'], paths: [`staff/${chosen.fields.staff}.md`] }))
|
|
42
|
+
.documents;
|
|
43
|
+
const answer = await house.callExternal({
|
|
44
|
+
url: `https://crm.example/cards/${card}`,
|
|
45
|
+
method: 'PUT',
|
|
46
|
+
headers: { 'content-type': 'application/json', 'idempotency-key': event.occurrence },
|
|
47
|
+
body: JSON.stringify({ status: 'assigned', manager: manager.fields.manager_id }),
|
|
48
|
+
room_ref: event.room,
|
|
49
|
+
});
|
|
50
|
+
if (answer.status !== 200) throw new Error(`the CRM answered ${answer.status}: ${answer.body}`);
|
|
51
|
+
const saved = await house.saveParts(event.room, [
|
|
52
|
+
{
|
|
53
|
+
path: `decisions/${card}.md`,
|
|
54
|
+
parts: {
|
|
55
|
+
creation: {
|
|
56
|
+
type: 'decision',
|
|
57
|
+
fields: { card, staff: chosen.fields.staff, decided_at: now },
|
|
58
|
+
sections: {},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
if (saved.outcome !== 'saved') throw new Error(JSON.stringify(saved));
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
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 and tells the Room's owner; the next lead still runs.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# App functions
|
|
2
|
+
|
|
3
|
+
How an App declares a function that House runs on its servers when a Trigger fires, with no page open and no model.
|
|
4
|
+
|
|
5
|
+
- **Declare.** `app.json` lists `functions`, each `{"name": "assign lead", "entry": "functions/assign.js"}` ([App manifest](reference/app-manifest)). The entry is one ES module whose default export is the function and which imports nothing, so your build bundles each function, with all it imports, into `dist/`, such as `esbuild src/assign.ts --bundle --format=esm --outfile=dist/functions/assign.js`. A publish is refused whole for an entry that is missing, imports, has no default export or is over <function-size>, for a name used twice, and for more than <release-functions> functions.
|
|
6
|
+
- **Run.** A Trigger's `function` action names the App and one of its functions ([Triggers](reference/triggers)). House runs the App's latest release when the occurrence's turn comes, one occurrence of a Trigger after another, so a run reads what the run before it wrote.
|
|
7
|
+
- **The event.** `export default async (event, house) => {}` receives `kind`, the condition's `on`, with `room`, `trigger`, `occurrence`, `moment` and what matched: `path` and `reference`, a document change's `transition`, an arriving item's `item` with its `headers` and raw `body` text, or a dated document's `document`. `Date` reads `moment`. The return value is ignored, so write what you keep into the Room.
|
|
8
|
+
- **The App API.** `house` is the page's App API over the Trigger's Room alone: `query(q)` ([Local Query](apps/local-query)), `saveParts(room, parts)`, `call(name, input)` for the Room acts such as `record.list` and `member.list`, and `callExternal(request)`. `query` and `callExternal` reject a refusal with its `code` for the function to catch, while `call` answers it as a result whose `kind` is `rejected` and `saveParts` as `outcome: 'refused'`, as on the page, so check what they answer. Its writes are the App's, by `App <App> (function <function>)`, fire Triggers, its own included, and count against the Room owner's Write limit.
|
|
9
|
+
- **Reach.** A run holds no `fetch`, timer, file, `console` or `Intl`: keep times as ISO instants, or convert a local time by its UTC offset yourself. `callExternal` takes the request `bridge.callExternal` takes and is the one way out, to an origin the release's `external_origins` lists; naming its Room, `room_ref: event.room`, carries that Room's Service key, which the function never sees, or is refused `service_key_not_found` naming the `origin`. Its calls count against the Room owner's `external_calls` limit.
|
|
10
|
+
- **Never repeated.** A run that started is never run again, even when it failed after a call reached the service. Send `event.occurrence` as the service's idempotency key where it takes one, and name what you write by what the event carries, so a second delivery of the same thing finds it.
|
|
11
|
+
- **Failure and limits.** A throw settles the run `failed` with `function_failed` and its text, which `list_trigger_occurrences` shows and House tells the Room's owner; the next occurrence still runs. A run ends `failed` at <function-time>, at <function-memory> or past <function-calls> external calls: `time_cap_reached`, `memory_cap_reached` or `external_call_cap_reached`.
|
|
12
|
+
|
|
13
|
+
Next: [Example: assign a lead](apps/example-function)
|
|
@@ -10,7 +10,7 @@ How an App reads and writes its Rooms on the device, offline included.
|
|
|
10
10
|
- A document has one Room Draft, shared by every tab and App, and `save` takes all of it. A write replaces every part and names the revision it read, so write onto the parts it holds; one from an older revision answers `stale` with the current Room Draft: show that one. A field part names as its `base` the value the page showed, `{ value }`, or `null` when the document has no such field.
|
|
11
11
|
- Save a change the User does not type, such as a checkbox, with `saveParts(room, [{ path, parts }])`: it writes no Room Draft, so the one another tab holds stays.
|
|
12
12
|
- Offline, all of this works and saves wait. `bridge.onStatus` reports `offline`; say so quietly.
|
|
13
|
-
- A Room your User
|
|
14
|
-
- An outside service is reached only at the origins `app.json` lists in `external_origins`, directly or through `bridge.callExternal`, which House's server sends;
|
|
13
|
+
- A Room your User holds at the Open level locks once the browser has been offline 2 minutes, and from the moment it opens until House confirms the access. While it is locked, `rooms` lists it `locked: true`, a query or search that does not name it leaves it out, one naming it answers `{ pending }` with `awaits: 'unlock'`, and every other read and write naming it, `save` included, answers the code `room-locked` and keeps nothing. Draw it as locked until it unlocks and keep what the User types in the page; what was saved before stays and is sent then.
|
|
14
|
+
- An outside service is reached only at the origins `app.json` lists in `external_origins`, directly or through `bridge.callExternal`, which House's server sends. A call that names a Room bound to the App in a `write` slot with `room_ref` carries that Room's Service key for the origin, which the page never sees; any other call carries no key.
|
|
15
15
|
- The page's own storage is a disposable cache, the Room the only durable home of the App's data; handle any code or value you do not know through `code` and `retryable`, which every refusal and failure carries.
|
|
16
16
|
- React: `useObserve` and `useSave` from `@agentshouse/mdruntime/react`. react-admin and Refine: the data providers of `@agentshouse/mdruntime-families`. The whole client is described in [The App runtime](apps/runtime).
|
|
@@ -42,4 +42,4 @@ read-only instead of retrying. `observe('readiness')` tells whether code and bou
|
|
|
42
42
|
locally (`ready`), each Room's state, each refused Room's file and rule, the persistence answer, `space`
|
|
43
43
|
(`full` once the device's storage refused a write, Save and messages refusing as `durable-write` until
|
|
44
44
|
`retry()` or a later test write finds space, or once the origin refused a newer release), and the
|
|
45
|
-
`installation`'s Rooms, parts, bytes, `
|
|
45
|
+
`installation`'s Rooms, parts, bytes, `step` and `paused`.
|
package/guide/index.md
CHANGED
|
@@ -6,7 +6,7 @@ House keeps Markdown files in Rooms, which your User shares with other people an
|
|
|
6
6
|
|
|
7
7
|
While your User's My Room, which `list_rooms` marks, holds no `wayfinding-map`, open [House Onboarding](skills/house-onboarding) first.
|
|
8
8
|
|
|
9
|
-
- `apps/`, building an App over your User's Rooms: [Build an App](apps/build).
|
|
10
|
-
- `reference/`, the formats House reads: [
|
|
9
|
+
- `apps/`, building an App over your User's Rooms: [Build an App](apps/build), and the functions House runs for it when a Trigger fires: [App functions](apps/functions).
|
|
10
|
+
- `reference/`, the formats House reads: [Access level](reference/access-level), [App manifest](reference/app-manifest), [Routine](reference/routine), [Schema](reference/schema) and [Triggers](reference/triggers).
|
|
11
11
|
- `routines/`, the Routines House writes into every Private Room: [Work on ticket](routines/work-on-ticket).
|
|
12
12
|
- `skills/`, House's Skills: [Prototype an App](skills/prototype-app).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Access level
|
|
2
|
+
|
|
3
|
+
How much of a Room a grant or a Share lets its holder do with its text, and where.
|
|
4
|
+
|
|
5
|
+
Access level, `level` on a Room grant or a Share’s Room item, each level including the one before it: `search`, the Search level, reaches the Room through `search`, `shell` and `inspect` alone, each reply at most <mcp-read-size> of the Room’s text, with no copy anywhere; `open`, the Open level, also opens the Room in browser Apps while the browser is online, and acts as `search` everywhere else; `keep`, the Keep level, copies the Room to the browser and the Environments, offline included, and is the one `export` needs. Apps need `open` or `keep`.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{ "room_ref": "r_ab12.1", "level": "open", "operations": ["write"] }
|
|
9
|
+
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
What an App's `app.json` declares.
|
|
4
4
|
|
|
5
|
-
App manifest, `app.json` at an App’s root: `entry`, its HTML page, and `room_slots`, each with a `name`, `access` `read` or `write`, and `types` from the `schema.yaml` beside it, written like a Room schema; `set_app_bindings` fills a slot with Rooms. Where needed it adds `external_origins`, the https origins its page may reach; `conversations: true` to read your User’s conversations; `local_search: true` to call `search`; `inboxes: true` to show and answer customer chats, naming `inbox-chat` and `inbox-message` in `room_slots`; `house_model`, the House model types it shows, which it reads from your User’s Private Room under `house/` without binding that Room; `house_acts`, the House acts it calls by their act names, such as `rooms/rename`, never one only a person takes or one that changes what an App reaches; and `theme_colour`, `#rrggbb`. A User who does not own the App is asked once to allow the House model types and House acts it declares, and again only for those a later release adds; a declined one reads nothing and is refused. [Build an App](apps/build) shows how its page works.
|
|
5
|
+
App manifest, `app.json` at an App’s root: `entry`, its HTML page, and `room_slots`, each with a `name`, `access` `read` or `write`, and `types` from the `schema.yaml` beside it, written like a Room schema; `set_app_bindings` fills a slot with Rooms. Where needed it adds `external_origins`, the https origins its page may reach; `conversations: true` to read your User’s conversations; `local_search: true` to call `search`; `inboxes: true` to show and answer customer chats, naming `inbox-chat` and `inbox-message` in `room_slots`; `house_model`, the House model types it shows, which it reads from your User’s Private Room under `house/` without binding that Room; `house_acts`, the House acts it calls by their act names, such as `rooms/rename`, never one only a person takes or one that changes what an App reaches; `functions`, each a `name` and the `entry` file a Trigger runs on House's servers ([App functions](apps/functions)); and `theme_colour`, `#rrggbb`. A User who does not own the App is asked once to allow the House model types and House acts it declares, and again only for those a later release adds; a declined one reads nothing and is refused. [Build an App](apps/build) shows how its page works.
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -4,6 +4,6 @@ How a Trigger's `condition` and `actions` are written for `create_trigger` and `
|
|
|
4
4
|
|
|
5
5
|
A Room's Triggers are its read-only `house-trigger` documents, one at `house/triggers/<trigger>.md` for each, which House writes as the Trigger's acts left it.
|
|
6
6
|
|
|
7
|
-
`condition`: When it fires, e.g. `{on: "documents", scope: {type: "task"}, mode: "starts_matching", new_documents: false, match: {all: [{path: "/fields/status", eq: "review"}, {path: "/members/assignee", eq: "<User id>"}]}}`; for a new document in a folder, `{on: "documents", scope: {folder: "/answers"}, mode: "starts_matching", new_documents: true, match: {all: []}}`; for any edit of one document, typed or plain, `{on: "documents", scope: {document: "/notes/plan.md"}, mode: "field_changes", field: "/text"}`; on documents, `moved_between_rooms: true` fires only on a document moved in from or out to another Room, `false` only on any other change; for arriving items, `{on: "items", address: "<push address>", match: {path: "/json/ref", eq: "refs/heads/main"}}`. A clock is `{on: "clock", cron: "0 9 * * 1", time_zone: "Europe/Berlin"}`, or a day before each task is due: `{on: "clock", scope: {type: "task"}, date: "/fields/due", days: -1, at: "09:00", time_zone: "Europe/Berlin", match: <test>}`.
|
|
7
|
+
`condition`: When it fires, e.g. `{on: "documents", scope: {type: "task"}, mode: "starts_matching", new_documents: false, match: {all: [{path: "/fields/status", eq: "review"}, {path: "/members/assignee", eq: "<User id>"}]}}`; for a new document in a folder, `{on: "documents", scope: {folder: "/answers"}, mode: "starts_matching", new_documents: true, match: {all: []}}`; for any edit of one document, typed or plain, `{on: "documents", scope: {document: "/notes/plan.md"}, mode: "field_changes", field: "/text"}`; on documents, `moved_between_rooms: true` fires only on a document moved in from or out to another Room, `false` only on any other change; for arriving items, `{on: "items", address: "<push address>", match: {path: "/json/ref", eq: "refs/heads/main"}}`. A clock is `{on: "clock", cron: "0 9 * * 1", time_zone: "Europe/Berlin"}`, or a day before each task is due: `{on: "clock", scope: {type: "task"}, date: "/fields/due", days: -1, at: "09:00", time_zone: "Europe/Berlin", match: <test>}`. A `/fields/…` or `/members/…` path reads only what the document's own type declares, so under a folder scope a test matches only documents of a type that declares that field, House's built-in types among them: a document with no `type` has no fields, whatever its front matter holds, so give it a `type` naming one in `settings/schema.yaml` that declares the field.
|
|
8
8
|
|
|
9
|
-
`actions`: What it does; a `notify` action names whom it reaches, e.g. `{kind: "notify", title: "Ready to test", text: "A task waits.", to: {users: ["<User id>"], fields: ["/members/assignee"], roles: ["owner"]}}`, Users by id, the member a field of the matched document names, and every holder of a Schema Role; a `routine` action names a Routine Access.
|
|
9
|
+
`actions`: What it does; a `notify` action names whom it reaches, e.g. `{kind: "notify", title: "Ready to test", text: "A task waits.", to: {users: ["<User id>"], fields: ["/members/assignee"], roles: ["owner"]}}`, Users by id, the member a field of the matched document names, and every holder of a Schema Role; a `routine` action names a Routine Access; a `function` action names an App and one function its latest release declares, e.g. `{kind: "function", app: "a_…", function: "assign lead"}`, which you may add only for an App you hold that has this Room in a write slot of your bindings; [App functions](apps/functions) says how House runs it. `list_trigger_occurrences` shows each run `ran` with its `release`, or `failed` with `function_failed` and its `error`, never run again, or `function_not_started` while House tries to start it, `final` after its last try.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/skills",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-ce95c9bef1e33",
|
|
4
4
|
"description": "The upstream Skills agents.house serves its agents, verbatim, and the texts it writes for them: the Guide, its own Skills among the pages, and the Agent instructions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Read level
|
|
2
|
-
|
|
3
|
-
How much of a Room a grant or a Share lets its holder read, and where.
|
|
4
|
-
|
|
5
|
-
Read level, `read` on a Room grant or a Share’s Room item, each level including the one before it: `mcp`, MCP Read, reads through `search`, `shell` and `inspect` alone, each reply at most <mcp-read-size> of the Room’s text, with no copy anywhere; `online`, Online Read, also opens the Room in browser Apps while the browser is online, and reads as `mcp` everywhere else; `local`, Local Read, copies the Room to the browser and the Environments, offline included, and is the one `export` needs. Apps need `online` or `local`.
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{ "room_ref": "r_ab12.1", "read": "online", "operations": ["write"] }
|
|
9
|
-
```
|