glove-foundry 0.0.0 → 0.2.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/dist/{chunk-UNHGCCSA.js → chunk-P65RT7H5.js} +486 -49
- package/dist/cli.js +536 -352
- package/dist/index.js +1 -1
- package/docs/building-with-foundry.md +39 -2
- package/docs/inspector.md +35 -2
- package/package.json +7 -6
- package/templates/minimal/README.md +58 -0
- package/templates/minimal/agents/assistant/agent.ts +28 -0
- package/templates/minimal/agents/assistant/tools/current-time.tool.ts +17 -0
- package/templates/minimal/env.example +2 -0
- package/templates/minimal/foundry.application.ts +12 -0
- package/templates/minimal/foundry.config.ts +6 -0
- package/templates/minimal/gitignore +3 -0
- package/templates/travel-concierge/README.md +203 -0
- package/templates/travel-concierge/agents/concierge/actions/reply.action.ts +6 -0
- package/templates/travel-concierge/agents/concierge/agent.ts +106 -0
- package/templates/travel-concierge/agents/concierge/apps/calendar.app.ts +67 -0
- package/templates/travel-concierge/agents/concierge/composition.ts +18 -0
- package/templates/travel-concierge/agents/concierge/events/message-received.event.ts +7 -0
- package/templates/travel-concierge/agents/concierge/events/message-sent.event.ts +7 -0
- package/templates/travel-concierge/agents/concierge/layers/trip-context.layer.ts +23 -0
- package/templates/travel-concierge/agents/concierge/memory/traveller.memory.ts +19 -0
- package/templates/travel-concierge/agents/concierge/predicates/mentions-trip.predicate.ts +17 -0
- package/templates/travel-concierge/agents/concierge/schedules/trip-countdown.ts +16 -0
- package/templates/travel-concierge/agents/concierge/subscribers/usage.subscriber.ts +16 -0
- package/templates/travel-concierge/agents/concierge/tools/find-flights.tool.ts +49 -0
- package/templates/travel-concierge/agents/concierge/topology.ts +35 -0
- package/templates/travel-concierge/agents/concierge/transmissions/messaging.transmission.ts +57 -0
- package/templates/travel-concierge/agents/concierge/workbench.ts +26 -0
- package/templates/travel-concierge/env.example +3 -0
- package/templates/travel-concierge/foundry.application.ts +18 -0
- package/templates/travel-concierge/foundry.config.ts +9 -0
- package/templates/travel-concierge/gitignore +3 -0
- package/templates/travel-concierge/lib/demo-model.ts +72 -0
- package/templates/travel-concierge/src/client.ts +22 -0
package/dist/index.js
CHANGED
|
@@ -5,15 +5,52 @@ Foundry uses the filesystem for code identity and imported values for code relat
|
|
|
5
5
|
## Create and run
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx glove foundry support-workforce
|
|
8
|
+
npx glove foundry init support-workforce
|
|
9
9
|
cd support-workforce
|
|
10
|
-
pnpm install
|
|
11
10
|
cp .env.example .env.local
|
|
11
|
+
pnpm install
|
|
12
12
|
pnpm dev
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
`glove foundry dev` discovers the source graph, derives identities, checks types and conventions, generates `.foundry/routes.d.ts`, and starts the runtime and inspector.
|
|
16
16
|
|
|
17
|
+
The generated project depends on the exact Glove versions the `glove-foundry` that created it was built against. That is not tidiness: a narrower range makes the package manager install a second copy of `glove-js`, and the two `JsSession` classes then fail to type-match.
|
|
18
|
+
|
|
19
|
+
### Templates
|
|
20
|
+
|
|
21
|
+
| `--template` | What you get |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `travel-concierge` (default) | A travel agent that searches flights, installs a calendar application, replies over a chat transmission, remembers the traveller, wakes on a schedule, and owns a sandboxed VFS and REPL. One example per convention. |
|
|
24
|
+
| `minimal` | One agent and one tool. |
|
|
25
|
+
|
|
26
|
+
The travel concierge runs before you configure a provider — `lib/demo-model.ts` answers deterministically until `OPENROUTER_API_KEY` is set, so the first run produces a real trace with real tool calls.
|
|
27
|
+
|
|
28
|
+
### Adding Foundry to an existing Next.js app
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd my-next-app
|
|
32
|
+
npx glove foundry init . --target nextjs # or just: npx glove foundry init .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A directory holding a Next.js app is detected, so `--target` is usually unnecessary. Foundry then joins the project rather than taking it over:
|
|
36
|
+
|
|
37
|
+
| Path | What lands there |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `foundry/agents/**` | The agents |
|
|
40
|
+
| `foundry/foundry.application.ts` | Data adapter, accounts, routes |
|
|
41
|
+
| `foundry/package.json` | `{"type":"module"}`, scoping ESM to the agents |
|
|
42
|
+
| `foundry.config.mts` | Points the runtime at `foundry/agents` |
|
|
43
|
+
| `lib/foundry.ts` | A typed client your app imports |
|
|
44
|
+
| `app/api/<agent>/route.ts` | An example route handler |
|
|
45
|
+
|
|
46
|
+
Your `package.json` keeps its name, scripts, and dependencies; it gains `foundry:dev`, `foundry:start`, and the Glove packages. Nothing it already owns is overwritten.
|
|
47
|
+
|
|
48
|
+
The runtime is a separate process from `next dev`, deliberately — it holds durable state and should not restart when a component changes. The app reaches it over HTTP through `lib/foundry.ts`, so no part of the agent graph is bundled into your app, and `FoundryRoutes` stays a type-only import.
|
|
49
|
+
|
|
50
|
+
Two details make this work in a Next.js project specifically. A Next.js app is not `"type": "module"`, so Node would load the agents through the CommonJS resolver and fail on Foundry's ESM-only export map; the nested `foundry/package.json` scopes ESM to the agent tree, and `.mts` makes the config unambiguous whatever the root declares.
|
|
51
|
+
|
|
52
|
+
In production, set `FOUNDRY_URL` to wherever the runtime is deployed and keep it private to your network — the inspector is a development surface.
|
|
53
|
+
|
|
17
54
|
## The filesystem is the static registry
|
|
18
55
|
|
|
19
56
|
```text
|
package/docs/inspector.md
CHANGED
|
@@ -27,7 +27,9 @@ Open **Runs**, then choose one invocation. The run detail starts with a four-pha
|
|
|
27
27
|
3. Agent work: observable model and tool work occurred.
|
|
28
28
|
4. Completed, failed, cancelled, or still in progress.
|
|
29
29
|
|
|
30
|
-
The event trace below the spine is collapsed by default.
|
|
30
|
+
The event trace below the spine is collapsed by default. Each row carries its wall-clock time and its offset from the start of the run, so a slow phase is visible without arithmetic. Filter the trace by event category, expand an event when you need its adapter payload, and copy a payload straight from the expanded row. The inspector shows observable intent, actions, and outcomes; it does not expose a model's private hidden chain-of-thought.
|
|
31
|
+
|
|
32
|
+
A failed run states its error above the spine rather than only inside the recorded output. A run that is still going says so and keeps a running duration. **Run again** reopens the run drawer with the same instance and message.
|
|
31
33
|
|
|
32
34
|
## Starting work
|
|
33
35
|
|
|
@@ -38,9 +40,40 @@ Use **New run** from any page. Select a definition and either:
|
|
|
38
40
|
|
|
39
41
|
The inspector reuses that instance's latest conversation or creates its first conversation, sends the message, then navigates directly to the new run.
|
|
40
42
|
|
|
43
|
+
## Filtering runs
|
|
44
|
+
|
|
45
|
+
The **Runs** page keeps its filters in the query string, so any view is a link you can send or bookmark:
|
|
46
|
+
|
|
47
|
+
| Parameter | Values |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `status` | `running`, `completed`, `failed`, or `cancelled`. `running` also covers pending runs. |
|
|
50
|
+
| `source` | Any recorded source kind, such as `direct`, `transmission`, `activation`, or `spawn`. |
|
|
51
|
+
| `q` | Free text matched against the agent, run id, instance id, and recorded input. |
|
|
52
|
+
|
|
53
|
+
`/runs?status=failed&q=invoice` opens directly on the failed runs mentioning an invoice. The status tabs carry live counts, and each row shows the run's duration alongside a relative start time.
|
|
54
|
+
|
|
41
55
|
## Live updates and search
|
|
42
56
|
|
|
43
|
-
The inspector subscribes to `/api/events` with server-sent events and also performs a low-frequency reconciliation.
|
|
57
|
+
The inspector subscribes to `/api/events` with server-sent events and also performs a low-frequency reconciliation. A busy run emits many events, so they are coalesced into one refresh rather than one repaint each.
|
|
58
|
+
|
|
59
|
+
A refresh preserves what you are doing: scroll position, an expanded event, an open `Output` panel, and the text and caret in a filter box all survive it. Relative timestamps and the duration of an in-flight run tick every second without a repaint.
|
|
60
|
+
|
|
61
|
+
Press `Command-K` or `Control-K` to search pages, definitions, instances, and retained runs.
|
|
62
|
+
|
|
63
|
+
## Keyboard
|
|
64
|
+
|
|
65
|
+
| Key | Action |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `j` / `k` | Move down and up the current list |
|
|
68
|
+
| `Enter` or `o` | Open the highlighted row |
|
|
69
|
+
| `/` | Focus the run filter, or open search elsewhere |
|
|
70
|
+
| `c` | Start a run |
|
|
71
|
+
| `r` | Refresh runtime data |
|
|
72
|
+
| `Command-K` / `Control-K` | Search |
|
|
73
|
+
| `Escape` | Close the drawer or search |
|
|
74
|
+
| `Command-Enter` | Submit the run drawer |
|
|
75
|
+
|
|
76
|
+
Every truncated identifier in the inspector has a copy button, so the full run, instance, or conversation id is always retrievable.
|
|
44
77
|
|
|
45
78
|
## Operator API used by the inspector
|
|
46
79
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glove-foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An Effect-native, file-routed framework for typed, observable Glove agent applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"files": [
|
|
41
41
|
"dist",
|
|
42
42
|
"docs",
|
|
43
|
+
"templates",
|
|
43
44
|
"THIRD_PARTY_NOTICES.md"
|
|
44
45
|
],
|
|
45
46
|
"keywords": [
|
|
@@ -62,14 +63,14 @@
|
|
|
62
63
|
"station-schedules": "^2.2.0",
|
|
63
64
|
"tsx": "^4.21.0",
|
|
64
65
|
"zod": "^4.3.6",
|
|
65
|
-
"glove-core": "3.6.0",
|
|
66
|
-
"glove-js": "0.4.0",
|
|
67
66
|
"glove-mcp": "1.1.0",
|
|
67
|
+
"glove-js": "0.4.0",
|
|
68
|
+
"glove-memory": "1.1.0",
|
|
69
|
+
"glove-core": "3.6.0",
|
|
68
70
|
"glove-lisp": "0.4.0",
|
|
69
71
|
"glove-mesh": "0.1.1",
|
|
70
|
-
"glove-
|
|
71
|
-
"glove-working-environment": "0.6.0"
|
|
72
|
-
"glove-python": "0.3.0"
|
|
72
|
+
"glove-python": "0.3.0",
|
|
73
|
+
"glove-working-environment": "0.6.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
76
|
"@types/node": "^25.2.3",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A [Glove Foundry](https://github.com/porkytheblack/glove/tree/main/packages/glove-foundry) application.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cp .env.example .env.local # add your OPENROUTER_API_KEY
|
|
7
|
+
{{installCommand}}
|
|
8
|
+
{{devCommand}}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then open **http://127.0.0.1:4141** and press **Start a run**.
|
|
12
|
+
|
|
13
|
+
## The one idea to understand first
|
|
14
|
+
|
|
15
|
+
Foundry separates a **definition** (code — what an agent can assemble, at `agents/assistant/agent.ts`) from an **instance** (data — one persisted identity with its own context, installed apps, and conversations). One definition serves many instances.
|
|
16
|
+
|
|
17
|
+
**The filesystem is the registry.** `agents/assistant/agent.ts` *is* the agent `assistant`. There are no string ids to keep in sync.
|
|
18
|
+
|
|
19
|
+
## Adding capabilities
|
|
20
|
+
|
|
21
|
+
Any file matching these names under an agent folder is discovered automatically. Each default-exports one definition, and the **filename becomes its id**.
|
|
22
|
+
|
|
23
|
+
| File | Defines | Helper |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `agent.ts` | The agent | `defineAgent` |
|
|
26
|
+
| `tools/*.tool.ts` | A tool any agent can mount | `defineSharedTool` |
|
|
27
|
+
| `apps/*.app.ts` | An installable capability bundle | `defineApp` |
|
|
28
|
+
| `transmissions/*.transmission.ts` | An external transport shape | `defineTransmission` |
|
|
29
|
+
| `events/*.event.ts` | A transmission event | `defineTransmissionEvent` |
|
|
30
|
+
| `predicates/*.predicate.ts` | An inbound match rule | `defineTransmissionPredicate` |
|
|
31
|
+
| `mcp/*.mcp.ts` | An MCP server entry | `defineMcp` |
|
|
32
|
+
| `memory/*.memory.ts` | A memory profile | `defineMemory` |
|
|
33
|
+
| `layers/*.layer.ts` | Native Glove setup | `defineLayer` |
|
|
34
|
+
| `subscribers/*.subscriber.ts` | An observer | `defineSubscriber` |
|
|
35
|
+
| `schedules/*.ts` | Recurring or future work | `defineSchedule` |
|
|
36
|
+
|
|
37
|
+
Create the file, add it to `composeAgent(...)`, and the dev server picks it up and regenerates types.
|
|
38
|
+
|
|
39
|
+
Want a worked example with a calendar application, a chat transport, memory, a schedule, and a sandboxed REPL? Scaffold the travel concierge:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx glove-foundry init my-concierge --template travel-concierge
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
| Command | What it does |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `{{devCommand}}` | Discover agents, typecheck, generate routes, serve the runtime and inspector |
|
|
50
|
+
| `{{startCommand}}` | Run without file watching |
|
|
51
|
+
| `{{lintCommand}}` | Lint, including the Foundry file-routing rules |
|
|
52
|
+
| `{{typecheckCommand}}` | `tsc --noEmit` |
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
- [Building with Foundry](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/building-with-foundry.md)
|
|
57
|
+
- [Architecture](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/architecture.md)
|
|
58
|
+
- [The inspector](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/inspector.md)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MemoryStore } from "glove-core";
|
|
2
|
+
import { createAdapter } from "glove-core/models/providers";
|
|
3
|
+
import { composeAgent, defineAgent } from "glove-foundry";
|
|
4
|
+
import currentTime from "./tools/current-time.tool.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The file path is the identity: this is the agent `assistant`.
|
|
8
|
+
*
|
|
9
|
+
* Every field may be a value or a function; a function is called per run with
|
|
10
|
+
* the request context. Add capabilities as sibling files — `tools/*.tool.ts`,
|
|
11
|
+
* `apps/*.app.ts`, `memory/*.memory.ts` — and compose them below.
|
|
12
|
+
*/
|
|
13
|
+
export default defineAgent({
|
|
14
|
+
description: "A general-purpose Glove assistant",
|
|
15
|
+
components: composeAgent(currentTime),
|
|
16
|
+
store: ({ conversationId }) => new MemoryStore(`assistant:${conversationId}`),
|
|
17
|
+
model: () => createAdapter({
|
|
18
|
+
provider: "openrouter",
|
|
19
|
+
model: process.env.OPENROUTER_MODEL ?? "openai/gpt-4.1-mini",
|
|
20
|
+
stream: true,
|
|
21
|
+
}),
|
|
22
|
+
systemPrompt: (_agent, { message, history }) => [
|
|
23
|
+
"You are a precise, practical assistant.",
|
|
24
|
+
`Current request: ${message.text}`,
|
|
25
|
+
`Prior messages: ${history.length}`,
|
|
26
|
+
].join("\n"),
|
|
27
|
+
compactionInstructions: () => "Preserve decisions, open work, and important context.",
|
|
28
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineSharedTool } from "glove-foundry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/** One file, one default export. The filename is the id: `current-time`. */
|
|
5
|
+
const currentTime = defineSharedTool({
|
|
6
|
+
description: "Return the current ISO timestamp",
|
|
7
|
+
tool: {
|
|
8
|
+
name: "current_time",
|
|
9
|
+
description: "Return the current ISO timestamp",
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
async do() {
|
|
12
|
+
return { status: "success" as const, data: new Date().toISOString() };
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default currentTime;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MemoryFoundryDataAdapter, defineApplication } from "glove-foundry";
|
|
2
|
+
|
|
3
|
+
/** Swap this adapter for your own to persist instances across restarts. */
|
|
4
|
+
export const data = new MemoryFoundryDataAdapter();
|
|
5
|
+
|
|
6
|
+
export default defineApplication({
|
|
7
|
+
name: "{{projectName}}",
|
|
8
|
+
data,
|
|
9
|
+
accounts: [],
|
|
10
|
+
routes: [],
|
|
11
|
+
bindings: [],
|
|
12
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A [Glove Foundry](https://github.com/porkytheblack/glove/tree/main/packages/glove-foundry) application. The example agent is a **travel concierge**: it searches flights, checks a calendar, remembers the traveller, replies over a chat transport, and wakes itself on a schedule.
|
|
4
|
+
|
|
5
|
+
It runs before you configure anything — there is a built-in demo model, so you get real runs and a real event trace with no API key.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env.local # optional: add OPENROUTER_API_KEY for real answers
|
|
9
|
+
{{installCommand}}
|
|
10
|
+
{{devCommand}}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open **http://127.0.0.1:4141** and press **Start a run**.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The one idea to understand first
|
|
18
|
+
|
|
19
|
+
Foundry separates two things that most frameworks merge:
|
|
20
|
+
|
|
21
|
+
| | What it is | Where it lives | Changes when |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| **Definition** | Code. What an agent *can* assemble. | `agents/concierge/agent.ts` | You edit a file |
|
|
24
|
+
| **Instance** | Data. One persisted identity, with its own context, installed apps, and conversations. | Your data adapter | You call the API or use the inspector |
|
|
25
|
+
|
|
26
|
+
One definition serves many instances. Two travellers can run the same concierge with different calendars, budgets, and chat accounts — without a branch in your code.
|
|
27
|
+
|
|
28
|
+
**The filesystem is the registry.** `agents/concierge/agent.ts` *is* the agent `concierge`. There are no string ids to keep in sync, and renaming a file breaks the import at compile time rather than at 3am.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What is in this project
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
agents/concierge/
|
|
36
|
+
agent.ts the agent. start here
|
|
37
|
+
composition.ts composeAgent(...) — what it is built from
|
|
38
|
+
tools/find-flights.tool.ts a shared tool
|
|
39
|
+
apps/calendar.app.ts an application an instance installs
|
|
40
|
+
transmissions/messaging.transmission.ts Telegram/WhatsApp-shaped transport
|
|
41
|
+
events/message-received.event.ts an inbound event
|
|
42
|
+
events/message-sent.event.ts an outbound event
|
|
43
|
+
predicates/mentions-trip.predicate.ts routing logic, kept out of the agent
|
|
44
|
+
memory/traveller.memory.ts ambient context across conversations
|
|
45
|
+
schedules/trip-countdown.ts recurring work
|
|
46
|
+
layers/trip-context.layer.ts direct access to the native Glove runtime
|
|
47
|
+
subscribers/usage.subscriber.ts observation without behaviour change
|
|
48
|
+
workbench.ts the agent's VFS + JavaScript REPL
|
|
49
|
+
topology.ts accounts and routes (runtime data)
|
|
50
|
+
|
|
51
|
+
foundry.application.ts data adapter, accounts, routes
|
|
52
|
+
foundry.config.ts port, execution policy
|
|
53
|
+
lib/demo-model.ts keyless model — delete once you have a key
|
|
54
|
+
src/client.ts a typed client for these agents
|
|
55
|
+
.foundry/routes.d.ts generated. do not edit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conventions
|
|
59
|
+
|
|
60
|
+
Any file matching these names under an agent folder is discovered automatically. Each one default-exports a single definition, and its **filename becomes its id**. Nested folders nest the id: `tools/calendar/today.tool.ts` is `calendar/today`.
|
|
61
|
+
|
|
62
|
+
| File | Defines | Helper |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `agent.ts` | The agent | `defineAgent` |
|
|
65
|
+
| `tools/*.tool.ts` | A tool any agent can mount | `defineSharedTool` |
|
|
66
|
+
| `apps/*.app.ts` | An installable capability bundle | `defineApp` |
|
|
67
|
+
| `transmissions/*.transmission.ts` | An external transport shape | `defineTransmission` |
|
|
68
|
+
| `events/*.event.ts` | A transmission event | `defineTransmissionEvent` |
|
|
69
|
+
| `predicates/*.predicate.ts` | An inbound match rule | `defineTransmissionPredicate` |
|
|
70
|
+
| `mcp/*.mcp.ts` | An MCP server entry | `defineMcp` |
|
|
71
|
+
| `memory/*.memory.ts` | A memory profile | `defineMemory` |
|
|
72
|
+
| `layers/*.layer.ts` | Native Glove setup | `defineLayer` |
|
|
73
|
+
| `subscribers/*.subscriber.ts` | An observer | `defineSubscriber` |
|
|
74
|
+
| `schedules/*.ts` | Recurring or future work | `defineSchedule` |
|
|
75
|
+
| `connections/*.connection.ts` | A long-lived inbound worker | `defineConnection` |
|
|
76
|
+
| `actions/*.action.ts` | A playbook action | `definePlaybookAction` |
|
|
77
|
+
|
|
78
|
+
Every field of `defineAgent` accepts **a value or a function**. A function runs per request with the full context — message, history, instance, installations — which is how one definition adapts without branching inside a prompt.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Things you will want to do
|
|
83
|
+
|
|
84
|
+
### Add a tool
|
|
85
|
+
|
|
86
|
+
Create `agents/concierge/tools/weather.tool.ts`, default-export `defineSharedTool({...})`, then add it to `composition.ts`. That is the whole loop — the dev server picks it up and regenerates types.
|
|
87
|
+
|
|
88
|
+
### Add a second agent
|
|
89
|
+
|
|
90
|
+
Create `agents/<name>/agent.ts`. It is immediately routable, appears in the inspector, and is added to `FoundryRoutes` for the typed client.
|
|
91
|
+
|
|
92
|
+
### Give an instance a capability
|
|
93
|
+
|
|
94
|
+
Applications and MCP servers stay inert until an instance installs one. From the inspector, open the instance and install `calendar`; or over the API:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl -X PUT http://127.0.0.1:4141/api/installations \
|
|
98
|
+
-H 'content-type: application/json' \
|
|
99
|
+
-d '{"agentId":"<instance-id>","kind":"application","id":"calendar","config":{"calendarId":"primary"}}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Connect a real chat app
|
|
103
|
+
|
|
104
|
+
`transmissions/messaging.transmission.ts` has the shape; only `deliver` needs a real call.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
function deliver(input: { threadId: string; text: string }, provider: string) {
|
|
108
|
+
return Effect.tryPromise(async () => {
|
|
109
|
+
const response = await fetch(
|
|
110
|
+
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "content-type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ chat_id: input.threadId, text: input.text }),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
const body = await response.json() as { result: { message_id: number } };
|
|
118
|
+
return { externalMessageId: String(body.result.message_id) };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For inbound, point the provider's webhook at your own HTTP handler and call `dispatchInbound` with the route id. Foundry stores only the safe metadata on the account — the token stays in your environment, behind `accessRef`.
|
|
124
|
+
|
|
125
|
+
### Schedule work
|
|
126
|
+
|
|
127
|
+
Agents never call `setTimeout`. Add a `schedules/*.ts` definition, or let the agent create one at runtime through Foundry's scheduling tools. Either way it becomes a persisted activation you can see under **Automations**.
|
|
128
|
+
|
|
129
|
+
### Call agents from your own code
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { createFoundryClient } from "glove-foundry/client";
|
|
133
|
+
import type { FoundryRoutes } from "../.foundry/routes.js";
|
|
134
|
+
|
|
135
|
+
const foundry = createFoundryClient<FoundryRoutes>({ baseUrl: "http://127.0.0.1:4141" });
|
|
136
|
+
const agent = await foundry.agent("concierge").create({ workspaceId: "demo" });
|
|
137
|
+
const conversation = await foundry.createConversation(agent.id);
|
|
138
|
+
const run = await foundry.send(agent.id, conversation.id, "Find me a flight to Nairobi");
|
|
139
|
+
console.log((await run.wait()).output);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`src/client.ts` is a runnable version of this.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## The inspector
|
|
147
|
+
|
|
148
|
+
`{{devCommand}}` serves a runtime inspector at **http://127.0.0.1:4141**.
|
|
149
|
+
|
|
150
|
+
| Page | Answers |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| Overview | Is the runtime healthy, what is running, what needs attention |
|
|
153
|
+
| Agents | Which definitions exist and which instances were provisioned |
|
|
154
|
+
| Runs | Every invocation, with status, duration, and source |
|
|
155
|
+
| Run detail | The phase spine and the full observable event trace |
|
|
156
|
+
| Automations | Schedules, sleeping runs, playbook listeners, inbound workers |
|
|
157
|
+
| Integrations | Transmissions, accounts, routes, and bindings |
|
|
158
|
+
| Workspaces | Shared entries, inbox, tasks, and non-secret environment values |
|
|
159
|
+
|
|
160
|
+
Filters live in the URL, so `/runs?status=failed` is a link you can send. Press `⌘K` to search, `j`/`k` to move through a list, `c` to start a run.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Going to production
|
|
165
|
+
|
|
166
|
+
1. **Replace the data adapter.** `MemoryFoundryDataAdapter` in `foundry.application.ts` loses everything on restart. Implement `FoundryDataAdapter` against your database.
|
|
167
|
+
2. **Delete `lib/demo-model.ts`** and the fallback in `agent.ts` once `OPENROUTER_API_KEY` is set.
|
|
168
|
+
3. **Own your credentials.** Foundry stores account *references*, never secrets. Keep tokens in your own adapter or secret manager.
|
|
169
|
+
4. **Run `{{startCommand}}`** rather than `dev` — no file watching, no restart-on-change.
|
|
170
|
+
5. **Keep the ESLint preset.** `glove-foundry/eslint` rejects patterns that break file routing, such as a hand-written `id` on a file-routed definition.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## The Glove packages
|
|
175
|
+
|
|
176
|
+
| Package | What it gives you |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| [`glove-foundry`](https://www.npmjs.com/package/glove-foundry) | This framework: routing, runtime, inspector, client |
|
|
179
|
+
| [`glove-core`](https://www.npmjs.com/package/glove-core) | The agent runtime, model adapters, stores, tools |
|
|
180
|
+
| [`glove-js`](https://www.npmjs.com/package/glove-js) | The JavaScript REPL session used in `workbench.ts` |
|
|
181
|
+
| [`glove-python`](https://www.npmjs.com/package/glove-python) | A Python REPL, same shape |
|
|
182
|
+
| [`glove-lisp`](https://www.npmjs.com/package/glove-lisp) | A Lisp REPL, same shape |
|
|
183
|
+
| [`glove-working-environment`](https://www.npmjs.com/package/glove-working-environment) | The sandboxed VFS behind `defineWorkingEnvironment` |
|
|
184
|
+
| [`glove-memory`](https://www.npmjs.com/package/glove-memory) | Memory schemas and adapters |
|
|
185
|
+
| [`glove-mcp`](https://www.npmjs.com/package/glove-mcp) | MCP client and server support |
|
|
186
|
+
| [`glove-mesh`](https://www.npmjs.com/package/glove-mesh) | Multi-agent messaging |
|
|
187
|
+
|
|
188
|
+
Add an environment package when you need it — `glove-env-documents`, `glove-env-spreadsheets`, `glove-env-images`, `glove-env-slides`, `glove-env-render`, `glove-env-ocr`, `glove-env-media`, `glove-env-email`, `glove-env-zip`, `glove-env-motion` — and mount it in `workbench.ts`.
|
|
189
|
+
|
|
190
|
+
## Commands
|
|
191
|
+
|
|
192
|
+
| Command | What it does |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `{{devCommand}}` | Discover agents, typecheck, generate routes, serve the runtime and inspector |
|
|
195
|
+
| `{{startCommand}}` | Run without file watching |
|
|
196
|
+
| `{{lintCommand}}` | Lint, including the Foundry file-routing rules |
|
|
197
|
+
| `{{typecheckCommand}}` | `tsc --noEmit` |
|
|
198
|
+
|
|
199
|
+
## Documentation
|
|
200
|
+
|
|
201
|
+
- [Building with Foundry](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/building-with-foundry.md)
|
|
202
|
+
- [Architecture](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/architecture.md)
|
|
203
|
+
- [The inspector](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/inspector.md)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { MemoryStore } from "glove-core";
|
|
2
|
+
import { createAdapter } from "glove-core/models/providers";
|
|
3
|
+
import { composePlaybook, defineAgent } from "glove-foundry";
|
|
4
|
+
import { DemoModel } from "../../lib/demo-model.js";
|
|
5
|
+
import reply from "./actions/reply.action.js";
|
|
6
|
+
import calendar from "./apps/calendar.app.js";
|
|
7
|
+
import { conciergeComponents } from "./composition.js";
|
|
8
|
+
import messageReceived from "./events/message-received.event.js";
|
|
9
|
+
import messageSent from "./events/message-sent.event.js";
|
|
10
|
+
import tripContext from "./layers/trip-context.layer.js";
|
|
11
|
+
import travellerMemory from "./memory/traveller.memory.js";
|
|
12
|
+
import mentionsTrip from "./predicates/mentions-trip.predicate.js";
|
|
13
|
+
import tripCountdown from "./schedules/trip-countdown.js";
|
|
14
|
+
import usage from "./subscribers/usage.subscriber.js";
|
|
15
|
+
import { travellerInbound, travellerOutbound, travellerAccount } from "./topology.js";
|
|
16
|
+
import messaging from "./transmissions/messaging.transmission.js";
|
|
17
|
+
import { conciergeRepl, conciergeWorkspace } from "./workbench.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The file path is the identity: `agents/concierge/agent.ts` is the agent
|
|
21
|
+
* `concierge`. There is no id to keep in sync.
|
|
22
|
+
*
|
|
23
|
+
* Every field below may be a value or a function. A function is called per
|
|
24
|
+
* run with the request context, which is how one definition serves many
|
|
25
|
+
* instances without branching inside the prompt.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Runs with no API key so `pnpm dev` works before you have configured one. */
|
|
29
|
+
function model() {
|
|
30
|
+
if (!process.env.OPENROUTER_API_KEY) return new DemoModel();
|
|
31
|
+
return createAdapter({
|
|
32
|
+
provider: "openrouter",
|
|
33
|
+
model: process.env.OPENROUTER_MODEL ?? "openai/gpt-4.1-mini",
|
|
34
|
+
stream: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* When a chat message arrives on the inbound route and mentions a trip, answer
|
|
40
|
+
* it and deliver the reply back to the same thread.
|
|
41
|
+
*/
|
|
42
|
+
const travellerChat = composePlaybook({
|
|
43
|
+
name: "traveller-chat",
|
|
44
|
+
transmission: messaging,
|
|
45
|
+
match: {
|
|
46
|
+
event: messageReceived,
|
|
47
|
+
routes: [travellerInbound],
|
|
48
|
+
predicate: { definition: mentionsTrip, parameters: { any: ["trip", "flight", "hotel"] } },
|
|
49
|
+
},
|
|
50
|
+
directives: [{
|
|
51
|
+
action: reply,
|
|
52
|
+
instruction: "Answer the traveller's message. Keep it short enough to read on a phone.",
|
|
53
|
+
}],
|
|
54
|
+
applications: [calendar],
|
|
55
|
+
outbound: [{
|
|
56
|
+
route: travellerOutbound,
|
|
57
|
+
application: calendar,
|
|
58
|
+
account: travellerAccount,
|
|
59
|
+
applicationAccount: travellerAccount,
|
|
60
|
+
event: messageSent,
|
|
61
|
+
instruction: "Send the reply back to the traveller's thread.",
|
|
62
|
+
}],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default defineAgent({
|
|
66
|
+
description: "Plans trips: finds flights, checks the calendar, and keeps the traveller updated",
|
|
67
|
+
tags: ["travel", "example"],
|
|
68
|
+
components: conciergeComponents,
|
|
69
|
+
|
|
70
|
+
// Persisted context, per conversation.
|
|
71
|
+
memory: [travellerMemory],
|
|
72
|
+
store: ({ conversationId }) => new MemoryStore(`concierge:${conversationId}`),
|
|
73
|
+
model,
|
|
74
|
+
|
|
75
|
+
// The prompt is built per run, so it can name what is actually mounted.
|
|
76
|
+
systemPrompt: (_agent, { message, history, installations }) => [
|
|
77
|
+
"You are a travel concierge. You are practical and you never invent bookings.",
|
|
78
|
+
"Use find_flights for availability, and the calendar tools before proposing dates.",
|
|
79
|
+
`Traveller asked: ${message.text}`,
|
|
80
|
+
`Prior messages: ${history.length}.`,
|
|
81
|
+
`Installed for this instance: ${installations.map((item) => `${item.kind}:${item.id}`).join(", ") || "nothing yet"}.`,
|
|
82
|
+
].join("\n"),
|
|
83
|
+
|
|
84
|
+
// The sandbox. `budgetUsd` is read off the instance so two travellers differ.
|
|
85
|
+
workingEnvironment: conciergeWorkspace,
|
|
86
|
+
repl: (_agent, { agentId, agentInstance }) =>
|
|
87
|
+
conciergeRepl(agentId, Number(agentInstance.context.budgetUsd ?? 2_000)),
|
|
88
|
+
|
|
89
|
+
// Recurring work. An instance can opt out through its own context.
|
|
90
|
+
schedules: (_agent, { agentInstance }) =>
|
|
91
|
+
agentInstance.context.muteCountdown === true ? [] : [tripCountdown],
|
|
92
|
+
|
|
93
|
+
// A playbook can only bind a transmission that an installed application
|
|
94
|
+
// owns, so this one exists only once `calendar` is installed on the
|
|
95
|
+
// instance. That is lazy assembly in one line: the same code adapts to
|
|
96
|
+
// whatever each instance actually has.
|
|
97
|
+
playbooks: (_agent, { installations }) =>
|
|
98
|
+
installations.some((item) => item.kind === "application" && item.id === "calendar")
|
|
99
|
+
? [travellerChat]
|
|
100
|
+
: [],
|
|
101
|
+
|
|
102
|
+
compactionInstructions: () =>
|
|
103
|
+
"Keep the destination, dates, budget, confirmed bookings, and open decisions.",
|
|
104
|
+
layers: [tripContext],
|
|
105
|
+
subscribers: [usage],
|
|
106
|
+
});
|