@farm.js/agent 0.1.0-beta.0 → 0.1.0-beta.2
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 +132 -5
- package/dist/index.d.ts +74 -2
- package/dist/index.js +105 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# @farm.js/agent
|
|
2
2
|
|
|
3
|
-
Durable agents for Farm.js,
|
|
3
|
+
Durable agents for Farm.js, built and run by
|
|
4
|
+
[agent-unit](https://github.com/farming-labs/agent-unit). The integration serves the agents in
|
|
5
|
+
`src/agents` as runs that can pause for a person's approval, survive restarts and redeploys, and
|
|
6
|
+
resume without repeating a model call or a side effect.
|
|
4
7
|
|
|
5
|
-
See [Stability and Support](https://farmjs.dev/docs/stability)
|
|
8
|
+
This package is in beta. See [Stability and Support](https://farmjs.dev/docs/stability).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
6
11
|
|
|
7
12
|
```bash
|
|
8
|
-
|
|
13
|
+
pnpm add @farm.js/agent agent-unit
|
|
9
14
|
```
|
|
10
15
|
|
|
16
|
+
`agent-unit` is a dependency of this package. Add it to the app as well, because agent files import
|
|
17
|
+
`defineAgent` from it and clients import `agent-unit/client`. `@farm.js/core` is a peer dependency,
|
|
18
|
+
and the package needs Node.js 22.13 or newer.
|
|
19
|
+
|
|
20
|
+
## Configure
|
|
21
|
+
|
|
11
22
|
```ts
|
|
12
23
|
// farm.config.ts
|
|
13
24
|
import { agent } from "@farm.js/agent";
|
|
@@ -15,9 +26,125 @@ import { defineConfig } from "@farm.js/core";
|
|
|
15
26
|
|
|
16
27
|
export default defineConfig({
|
|
17
28
|
integrations: {
|
|
18
|
-
agent: agent({
|
|
29
|
+
agent: agent({
|
|
30
|
+
storage: { driver: "redis", url: process.env.REDIS_URL },
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`farm dev` serves the agents from source and reloads them on change. `farm build` builds them into
|
|
37
|
+
the server for the app's deploy target, with run storage and a scheduled sweep that wakes sleeping
|
|
38
|
+
runs and recovers stalled ones. Settings evaluate where the server runs, so a value read from
|
|
39
|
+
`process.env` is never written into the build.
|
|
40
|
+
|
|
41
|
+
## Write an agent
|
|
42
|
+
|
|
43
|
+
Each file in `src/agents` is an agent named after the file:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// src/agents/refund.ts
|
|
47
|
+
import { defineAgent } from "agent-unit";
|
|
48
|
+
|
|
49
|
+
export default defineAgent(async (input: { orderId: string }, run) => {
|
|
50
|
+
const order = await run.step("look up order", async () => ({ id: input.orderId, total: 42 }));
|
|
51
|
+
const { approved } = await run.interrupt<{ approved: boolean }>("approve-refund", order);
|
|
52
|
+
return approved ? `refunded ${order.id}` : `kept ${order.id}`;
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`run.step` runs once and is replayed from the run's journal afterwards. `run.interrupt` pauses the
|
|
57
|
+
run until an answer arrives. Agents written with the AI SDK, Mastra, LangGraph, or the OpenAI
|
|
58
|
+
Agents SDK work as a default export too; the framework is detected from `package.json`.
|
|
59
|
+
|
|
60
|
+
## Call the agents
|
|
61
|
+
|
|
62
|
+
The API is mounted at `basePath`, `/api/agent` by default, as ordinary API routes, so the app's
|
|
63
|
+
middleware and integration guards run first in development and in every deploy target.
|
|
64
|
+
|
|
65
|
+
| Request | Does |
|
|
66
|
+
| ---------------------------------------- | ---------------------------------------------------------------------- |
|
|
67
|
+
| `POST /api/agent/agents/:name/runs` | Starts a run with `{ input }`. `Accept: text/event-stream` streams it. |
|
|
68
|
+
| `GET /api/agent/runs/:id` | Reads a run: its status, pending interrupt, and output. |
|
|
69
|
+
| `GET /api/agent/runs/:id/events` | Streams its events, from `?after=` or `Last-Event-ID`. |
|
|
70
|
+
| `POST /api/agent/runs/:id/resume` | Answers the interrupt with `{ answer }`. |
|
|
71
|
+
| `GET /api/agent/runs?status=interrupted` | Lists runs, for an approvals inbox. |
|
|
72
|
+
| `GET /api/agent/manifest.json` | Lists the agents. |
|
|
73
|
+
| `POST /api/agent/mcp` | The agents as MCP tools. |
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { createAgentClient } from "agent-unit/client";
|
|
77
|
+
|
|
78
|
+
const agents = createAgentClient({ baseUrl: "/api/agent" });
|
|
79
|
+
|
|
80
|
+
for await (const event of agents.run("refund", { orderId: "o_1" })) {
|
|
81
|
+
if (event.type === "RUN_INTERRUPTED") console.log("needs approval", event.interrupt.payload);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The integration owns every path under `basePath`; `farm dev` refuses to start when an API route in
|
|
86
|
+
`src/app` sits under it.
|
|
87
|
+
|
|
88
|
+
## Authorize requests
|
|
89
|
+
|
|
90
|
+
Every request reaches `authorize` before the agents. Return `false` to answer `401`, or a
|
|
91
|
+
`Response` to answer with. The second argument says what the request does:
|
|
92
|
+
`{ action, agent, runId, threadId }`.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
agent({
|
|
96
|
+
authorize: (request) =>
|
|
97
|
+
request.headers.get("authorization") === `Bearer ${process.env.AGENT_TOKEN}`,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Set `authorize`, or a middleware guard on `basePath`, before anyone else can reach the app. Without
|
|
102
|
+
one, any caller can list, read, answer, and start runs. A guard should let agent-unit's internal
|
|
103
|
+
continue and sweep requests through; they carry `AGENT_UNIT_SECRET` as a bearer token, which
|
|
104
|
+
agent-unit checks itself.
|
|
105
|
+
|
|
106
|
+
## Options
|
|
107
|
+
|
|
108
|
+
| Option | Default | Description |
|
|
109
|
+
| ----------- | -------------------- | ------------------------------------------------------------ |
|
|
110
|
+
| `basePath` | `/api/agent` | Where the API is mounted. Must be under `/api`. |
|
|
111
|
+
| `agentsDir` | `agents` in `srcDir` | The folder of agent files, relative to the app root. |
|
|
112
|
+
| `agents` | | Agents as an object, instead of the folder. |
|
|
113
|
+
| `framework` | detected | `"ai-sdk"`, `"mastra"`, `"langgraph"`, or `"openai-agents"`. |
|
|
114
|
+
| `storage` | disk on Node | Run storage, as an unstorage driver config. |
|
|
115
|
+
| `presets` | | Settings per deploy target, over the top-level ones. |
|
|
116
|
+
| `authorize` | | Decides every request. |
|
|
117
|
+
| `name` | package name | The name in the manifest, MCP, and A2A card. |
|
|
118
|
+
| `tasks` | | Scheduled tasks run by the target's scheduler. |
|
|
119
|
+
| `budget` | per target | How long a run works before it yields and continues. |
|
|
120
|
+
| `retention` | kept | How long finished runs are kept. |
|
|
121
|
+
| `sweep` | every minute | The sweep's cron schedule, or `false`. |
|
|
122
|
+
| `adapters` | | Adapters for other agent frameworks. |
|
|
123
|
+
|
|
124
|
+
Without `storage`, a Node server keeps runs on disk in `.data/agent-unit`, and targets without a
|
|
125
|
+
durable disk keep them in memory. Use a shared driver in production, per target if needed:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
agent({
|
|
129
|
+
storage: { driver: "fs-lite", base: ".data/agents" },
|
|
130
|
+
presets: {
|
|
131
|
+
vercel: { storage: { driver: "upstash" } },
|
|
132
|
+
"cloudflare-module": { storage: { driver: "cloudflare-kv-binding", binding: "AGENTS" } },
|
|
19
133
|
},
|
|
20
134
|
});
|
|
21
135
|
```
|
|
22
136
|
|
|
23
|
-
|
|
137
|
+
`@farm.js/agent/config` is the module agent-unit reads these settings from inside the built
|
|
138
|
+
server; apps do not import it.
|
|
139
|
+
|
|
140
|
+
## Documentation
|
|
141
|
+
|
|
142
|
+
- [Durable agents](https://farmjs.dev/docs/integrations/agent)
|
|
143
|
+
- [Agent frameworks](https://farmjs.dev/docs/integrations/agent-frameworks), for chat routes that
|
|
144
|
+
do not need durable runs
|
|
145
|
+
|
|
146
|
+
The [`examples/agent`](https://github.com/farming-labs/farm.js/tree/main/examples/agent) app runs an
|
|
147
|
+
approval agent end to end.
|
|
148
|
+
|
|
149
|
+
Report issues and find examples in the
|
|
150
|
+
[Farm.js repository](https://github.com/farming-labs/farm.js).
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { AgentFramework } from 'agent-unit';
|
|
|
7
7
|
* Nitro and Workers settings, which belong in farm.config.
|
|
8
8
|
*/
|
|
9
9
|
interface AgentOptions extends Omit<AgentUnitConfig, "server" | "preset" | "runtime" | "cloudflare" | "nitro"> {
|
|
10
|
-
/** Where the agent API is mounted
|
|
10
|
+
/** Where the agent API is mounted, under `/api`. It owns every path under it. Default `/api/agent`. */
|
|
11
11
|
basePath?: string;
|
|
12
12
|
/** The folder of agent files, relative to the app root. Default `agents` in `srcDir`. */
|
|
13
13
|
agentsDir?: string;
|
|
@@ -28,7 +28,79 @@ declare function agent(options?: AgentOptions): _farm_js_core.DefinedIntegration
|
|
|
28
28
|
category: "agent";
|
|
29
29
|
type: string;
|
|
30
30
|
instance: AgentInstance;
|
|
31
|
-
plugins: _farm_js_core.FarmPlugin<undefined, Record<string, never>, unknown, undefined, unknown, false, _farm_js_core.
|
|
31
|
+
plugins: _farm_js_core.FarmPlugin<undefined, Record<string, never>, unknown, undefined, unknown, false, (_farm_js_core.RouteDefinition<`/${string}`, "POST", {
|
|
32
|
+
__types: {
|
|
33
|
+
body: unknown;
|
|
34
|
+
inputBody: never;
|
|
35
|
+
query: never;
|
|
36
|
+
inputQuery: never;
|
|
37
|
+
headers: never;
|
|
38
|
+
inputHeaders: never;
|
|
39
|
+
response: unknown;
|
|
40
|
+
errors: never;
|
|
41
|
+
};
|
|
42
|
+
__path?: string;
|
|
43
|
+
__method?: string;
|
|
44
|
+
__openapi?: _farm_js_core.EndpointOpenAPIMetadata;
|
|
45
|
+
__mcp?: _farm_js_core.EndpointMCPOptions;
|
|
46
|
+
} & ((options?: {
|
|
47
|
+
body?: undefined;
|
|
48
|
+
query?: undefined;
|
|
49
|
+
headers?: undefined;
|
|
50
|
+
} | undefined) => Promise<unknown>) & {
|
|
51
|
+
__types: {
|
|
52
|
+
params: {};
|
|
53
|
+
inputHeaders: never;
|
|
54
|
+
};
|
|
55
|
+
}> | _farm_js_core.RouteDefinition<`/${string}`, "GET", {
|
|
56
|
+
__types: {
|
|
57
|
+
body: unknown;
|
|
58
|
+
inputBody: never;
|
|
59
|
+
query: never;
|
|
60
|
+
inputQuery: never;
|
|
61
|
+
headers: never;
|
|
62
|
+
inputHeaders: never;
|
|
63
|
+
response: unknown;
|
|
64
|
+
errors: never;
|
|
65
|
+
};
|
|
66
|
+
__path?: string;
|
|
67
|
+
__method?: string;
|
|
68
|
+
__openapi?: _farm_js_core.EndpointOpenAPIMetadata;
|
|
69
|
+
__mcp?: _farm_js_core.EndpointMCPOptions;
|
|
70
|
+
} & ((options?: {
|
|
71
|
+
body?: undefined;
|
|
72
|
+
query?: undefined;
|
|
73
|
+
headers?: undefined;
|
|
74
|
+
} | undefined) => Promise<unknown>) & {
|
|
75
|
+
__types: {
|
|
76
|
+
params: {};
|
|
77
|
+
inputHeaders: never;
|
|
78
|
+
};
|
|
79
|
+
}> | _farm_js_core.RouteDefinition<`/${string}`, "DELETE", {
|
|
80
|
+
__types: {
|
|
81
|
+
body: unknown;
|
|
82
|
+
inputBody: never;
|
|
83
|
+
query: never;
|
|
84
|
+
inputQuery: never;
|
|
85
|
+
headers: never;
|
|
86
|
+
inputHeaders: never;
|
|
87
|
+
response: unknown;
|
|
88
|
+
errors: never;
|
|
89
|
+
};
|
|
90
|
+
__path?: string;
|
|
91
|
+
__method?: string;
|
|
92
|
+
__openapi?: _farm_js_core.EndpointOpenAPIMetadata;
|
|
93
|
+
__mcp?: _farm_js_core.EndpointMCPOptions;
|
|
94
|
+
} & ((options?: {
|
|
95
|
+
body?: undefined;
|
|
96
|
+
query?: undefined;
|
|
97
|
+
headers?: undefined;
|
|
98
|
+
} | undefined) => Promise<unknown>) & {
|
|
99
|
+
__types: {
|
|
100
|
+
params: {};
|
|
101
|
+
inputHeaders: never;
|
|
102
|
+
};
|
|
103
|
+
}>)[]>[];
|
|
32
104
|
}, _farm_js_core.FarmSchema | undefined>;
|
|
33
105
|
|
|
34
106
|
export { type AgentInstance, type AgentOptions, agent };
|
package/dist/index.js
CHANGED
|
@@ -8,58 +8,108 @@ function registerAgentUnitConfig(config) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// src/plugin.ts
|
|
11
|
+
import { writeFileSync } from "fs";
|
|
11
12
|
import path from "path";
|
|
12
|
-
import { Readable } from "stream";
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
import { definePlugin } from "@farm.js/core/plugin";
|
|
15
15
|
var loadBuildOnly = (specifier) => import(
|
|
16
16
|
/* @vite-ignore */
|
|
17
17
|
specifier
|
|
18
18
|
);
|
|
19
|
+
var AGENT_SERVER = /* @__PURE__ */ Symbol.for("farm.agent.server");
|
|
20
|
+
var METHODS = ["get", "post", "delete"];
|
|
19
21
|
function isUnder(pathname, basePath) {
|
|
20
22
|
return basePath === "/" || pathname === basePath || pathname.startsWith(`${basePath}/`);
|
|
21
23
|
}
|
|
24
|
+
var SERVER_MODULE = `// Generated by @farm.js/agent. Do not edit.
|
|
25
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
|
+
import { getUnit } from "./runtime.mjs";
|
|
27
|
+
|
|
28
|
+
const hostWaitUntil = new AsyncLocalStorage();
|
|
29
|
+
|
|
30
|
+
globalThis[Symbol.for("farm.agent.server")] = {
|
|
31
|
+
handle(request) {
|
|
32
|
+
const waitUntil = hostWaitUntil.getStore();
|
|
33
|
+
return getUnit().handler(request, waitUntil ? { waitUntil } : {});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default function farmAgentHost(event, next) {
|
|
38
|
+
const req = event.req;
|
|
39
|
+
return typeof req.waitUntil === "function"
|
|
40
|
+
? hostWaitUntil.run((promise) => req.waitUntil(promise), next)
|
|
41
|
+
: next();
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
var CRON_GUARD_MODULE = `// Generated by @farm.js/agent. Do not edit.
|
|
45
|
+
import { isCronRequestAuthorized } from "@farm.js/core/cron";
|
|
46
|
+
|
|
47
|
+
export default function farmAgentCronGuard(event, next) {
|
|
48
|
+
if (isCronRequestAuthorized(event.req)) return next();
|
|
49
|
+
return Response.json({ error: "Unauthorized cron request." }, { status: 401 });
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
function guardVercelCron(nitro, dir) {
|
|
53
|
+
const preset = String(nitro.options.preset ?? "");
|
|
54
|
+
if (!preset.startsWith("vercel")) return;
|
|
55
|
+
if (!nitro.options.experimental?.tasks) return;
|
|
56
|
+
if (Object.keys(nitro.options.scheduledTasks ?? {}).length === 0) return;
|
|
57
|
+
const route = nitro.options.vercel?.cronHandlerRoute || "/_vercel/cron";
|
|
58
|
+
const guard = path.posix.join(dir, "farm-cron-guard.mjs");
|
|
59
|
+
writeFileSync(guard, CRON_GUARD_MODULE);
|
|
60
|
+
nitro.options.handlers.unshift(
|
|
61
|
+
{ route, handler: guard, middleware: true, lazy: false },
|
|
62
|
+
{ route: `${route}/**`, handler: guard, middleware: true, lazy: false }
|
|
63
|
+
);
|
|
64
|
+
if (!process.env.CRON_SECRET) {
|
|
65
|
+
nitro.logger.warn(
|
|
66
|
+
`@farm.js/agent: Vercel Cron runs the agent sweep and tasks at ${route}, which rejects every call without CRON_SECRET. Set CRON_SECRET in the Vercel project's environment variables, or the scheduled runs answer 401.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
22
70
|
function createAgentPlugin(config) {
|
|
23
71
|
const { basePath } = config;
|
|
24
72
|
let root;
|
|
25
73
|
let agentsDir = config.agentsDir ?? "agents";
|
|
74
|
+
let isDev = false;
|
|
26
75
|
let devHandler;
|
|
76
|
+
const buildConfig = () => ({ ...config, agentsDir });
|
|
77
|
+
const startDev = () => devHandler ??= loadBuildOnly("agent-unit/build").then(
|
|
78
|
+
({ createDevHandler }) => createDevHandler({ root: root ?? process.cwd(), config: buildConfig() })
|
|
79
|
+
);
|
|
27
80
|
const closeDev = async () => {
|
|
28
81
|
const pending = devHandler;
|
|
29
82
|
devHandler = void 0;
|
|
30
83
|
await (await pending?.catch(() => void 0))?.close();
|
|
31
84
|
};
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Vite runs this again on every restart, while the agents keep running in one dev handler.
|
|
39
|
-
configureServer(server) {
|
|
40
|
-
devHandler ??= loadBuildOnly("agent-unit/build").then(
|
|
41
|
-
({ createDevHandler }) => createDevHandler({ root: root ?? process.cwd(), config: buildConfig() })
|
|
85
|
+
const handle = async (request) => {
|
|
86
|
+
if (isDev) return (await startDev()).fetch(request);
|
|
87
|
+
const server = globalThis[AGENT_SERVER];
|
|
88
|
+
if (!server) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"@farm.js/agent: this server was built without the agents. Build it with `farm build`, which adds them."
|
|
42
91
|
);
|
|
43
|
-
const handler = devHandler;
|
|
44
|
-
server.middlewares.use((req, res, next) => {
|
|
45
|
-
if (!isUnder((req.url ?? "/").split("?")[0], basePath)) return next();
|
|
46
|
-
void serveDev(handler, req, res).catch(next);
|
|
47
|
-
});
|
|
48
92
|
}
|
|
93
|
+
return server.handle(request);
|
|
49
94
|
};
|
|
50
95
|
return definePlugin({
|
|
51
96
|
name: "farm:agent",
|
|
52
97
|
configure(farmConfig, context) {
|
|
53
98
|
root = path.resolve(farmConfig.root ?? process.cwd());
|
|
54
99
|
agentsDir = config.agentsDir ?? path.posix.join(farmConfig.srcDir ?? "src", "agents");
|
|
55
|
-
|
|
56
|
-
const vite = farmConfig.vite ?? {};
|
|
57
|
-
return { ...farmConfig, vite: { ...vite, plugins: [devPlugin, ...vite.plugins ?? []] } };
|
|
100
|
+
isDev = context.isDev;
|
|
58
101
|
},
|
|
102
|
+
// Ordinary API routes, so a request passes Farm's middleware, runtime.before and integration
|
|
103
|
+
// guards before it reaches the agents, in development and in every production target.
|
|
104
|
+
routes: ({ route }) => METHODS.flatMap(
|
|
105
|
+
(method) => [basePath, `${basePath}/[...path]`].map(
|
|
106
|
+
(pattern) => route[method](pattern, { handler: (request) => handle(request) })
|
|
107
|
+
)
|
|
108
|
+
),
|
|
59
109
|
router: {
|
|
60
110
|
// agent-unit answers every path under basePath, so an app route there would never be reached.
|
|
61
111
|
discovered(route) {
|
|
62
|
-
if (route.kind !== "api" || !isUnder(route.path, basePath)) return;
|
|
112
|
+
if (route.kind !== "api" || !route.filePath || !isUnder(route.path, basePath)) return;
|
|
63
113
|
throw new Error(
|
|
64
114
|
`@farm.js/agent: ${route.filePath} is served at ${route.path}, which the agent API owns (basePath "${basePath}"). Move the route, or set another basePath.`
|
|
65
115
|
);
|
|
@@ -67,21 +117,49 @@ function createAgentPlugin(config) {
|
|
|
67
117
|
},
|
|
68
118
|
build: {
|
|
69
119
|
// agent-unit builds the agents into Farm's Nitro output for its preset: their framework
|
|
70
|
-
// adapters, storage, the sweep and `tasks` as scheduled tasks.
|
|
120
|
+
// adapters, storage, the sweep and `tasks` as scheduled tasks. Its own handler for basePath
|
|
121
|
+
// would answer before Farm's, so Farm's routes serve the API instead.
|
|
71
122
|
async configure(nitroConfig) {
|
|
72
123
|
const { agentUnit } = await loadBuildOnly("agent-unit/nitro");
|
|
73
|
-
const
|
|
124
|
+
const unit = agentUnit({
|
|
74
125
|
root: root ?? process.cwd(),
|
|
75
126
|
basePath,
|
|
76
127
|
config: buildConfig(),
|
|
77
128
|
// The module agent-unit's engine reads its config from in the built server.
|
|
78
129
|
configFile: fileURLToPath(new URL("./config.js", import.meta.url))
|
|
79
130
|
});
|
|
131
|
+
const module = {
|
|
132
|
+
name: unit.name,
|
|
133
|
+
async setup(nitro) {
|
|
134
|
+
const before = nitro.options.handlers.length;
|
|
135
|
+
await unit.setup(nitro);
|
|
136
|
+
const added = nitro.options.handlers.splice(before);
|
|
137
|
+
const entry = added.find((handler) => handler.handler.endsWith("entry.mjs"))?.handler;
|
|
138
|
+
if (!entry) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"@farm.js/agent: agent-unit's Nitro module did not mount its handler. Install the agent-unit version @farm.js/agent supports."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
nitro.options.handlers.push(...added.filter((handler) => handler.handler !== entry));
|
|
144
|
+
const server = path.posix.join(
|
|
145
|
+
path.posix.dirname(entry.replaceAll("\\", "/")),
|
|
146
|
+
"farm-server.mjs"
|
|
147
|
+
);
|
|
148
|
+
writeFileSync(server, SERVER_MODULE);
|
|
149
|
+
guardVercelCron(nitro, path.posix.dirname(server));
|
|
150
|
+
nitro.options.handlers.push(
|
|
151
|
+
{ route: basePath, handler: server, middleware: true, lazy: false },
|
|
152
|
+
{ route: `${basePath}/**`, handler: server, middleware: true, lazy: false }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
80
156
|
return { ...nitroConfig, modules: [...nitroConfig.modules ?? [], module] };
|
|
81
157
|
}
|
|
82
158
|
},
|
|
83
159
|
dev: {
|
|
84
160
|
server(_viteServer, { lifecycle }) {
|
|
161
|
+
void startDev().catch(() => {
|
|
162
|
+
});
|
|
85
163
|
lifecycle.onShutdown(closeDev);
|
|
86
164
|
}
|
|
87
165
|
},
|
|
@@ -90,33 +168,6 @@ function createAgentPlugin(config) {
|
|
|
90
168
|
}
|
|
91
169
|
});
|
|
92
170
|
}
|
|
93
|
-
async function serveDev(handler, req, res) {
|
|
94
|
-
const { sendWebResponse } = await loadBuildOnly("@farm.js/core/server");
|
|
95
|
-
const response = await (await handler).fetch(toWebRequest(req, res));
|
|
96
|
-
await sendWebResponse(res, response);
|
|
97
|
-
}
|
|
98
|
-
function toWebRequest(req, res) {
|
|
99
|
-
const protocol = req.socket.encrypted ? "https" : "http";
|
|
100
|
-
const url = new URL(req.url ?? "/", `${protocol}://${req.headers.host ?? "localhost"}`);
|
|
101
|
-
const headers = new Headers();
|
|
102
|
-
for (let index = 0; index < req.rawHeaders.length; index += 2) {
|
|
103
|
-
const name = req.rawHeaders[index];
|
|
104
|
-
if (!name.startsWith(":")) headers.append(name, req.rawHeaders[index + 1]);
|
|
105
|
-
}
|
|
106
|
-
const controller = new AbortController();
|
|
107
|
-
res.once("close", () => {
|
|
108
|
-
if (!res.writableFinished) controller.abort();
|
|
109
|
-
});
|
|
110
|
-
const method = req.method ?? "GET";
|
|
111
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
112
|
-
return new Request(url, {
|
|
113
|
-
method,
|
|
114
|
-
headers,
|
|
115
|
-
signal: controller.signal,
|
|
116
|
-
body: hasBody ? Readable.toWeb(req) : void 0,
|
|
117
|
-
...hasBody ? { duplex: "half" } : {}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
171
|
|
|
121
172
|
// src/index.ts
|
|
122
173
|
var DEFAULT_BASE_PATH = "/api/agent";
|
|
@@ -124,7 +175,11 @@ function normalizeBasePath(value) {
|
|
|
124
175
|
if (!value.startsWith("/")) {
|
|
125
176
|
throw new Error(`@farm.js/agent: basePath must start with "/", got "${value}".`);
|
|
126
177
|
}
|
|
127
|
-
|
|
178
|
+
const basePath = value.replace(/\/+$/, "");
|
|
179
|
+
if (basePath !== "/api" && !basePath.startsWith("/api/")) {
|
|
180
|
+
throw new Error(`@farm.js/agent: basePath must be under /api, got "${value}".`);
|
|
181
|
+
}
|
|
182
|
+
return basePath;
|
|
128
183
|
}
|
|
129
184
|
function agent(options = {}) {
|
|
130
185
|
const basePath = normalizeBasePath(options.basePath ?? DEFAULT_BASE_PATH);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/plugin.ts"],"sourcesContent":["import { defineIntegration } from \"@farm.js/core\";\nimport type { AgentFramework, AgentUnitConfig } from \"agent-unit\";\n\nimport { registerAgentUnitConfig } from \"./config.js\";\nimport { createAgentPlugin } from \"./plugin.js\";\n\nexport type { AgentFramework };\n\n/**\n * agent-unit's settings, minus what Farm owns: the host (`deploy.target`), the server and the\n * Nitro and Workers settings, which belong in farm.config.\n */\nexport interface AgentOptions extends Omit<\n AgentUnitConfig,\n \"server\" | \"preset\" | \"runtime\" | \"cloudflare\" | \"nitro\"\n> {\n /** Where the agent API is mounted. It owns every path under it. Default `/api/agent`. */\n basePath?: string;\n /** The folder of agent files, relative to the app root. Default `agents` in `srcDir`. */\n agentsDir?: string;\n}\n\nexport interface AgentInstance {\n readonly basePath: string;\n readonly framework?: AgentFramework;\n}\n\nconst DEFAULT_BASE_PATH = \"/api/agent\";\n\nfunction normalizeBasePath(value: string): string {\n if (!value.startsWith(\"/\")) {\n throw new Error(`@farm.js/agent: basePath must start with \"/\", got \"${value}\".`);\n }\n return value.replace(/\\/+$/, \"\") || \"/\";\n}\n\n/**\n * Durable agents in a Farm app: the agents in `src/agents`, written with the AI SDK, Mastra,\n * LangGraph, the OpenAI Agents SDK or agent-unit's `defineAgent`, served under `basePath` in\n * `farm dev` and built by agent-unit into every deployment target.\n *\n * @example\n * integrations: { agent: agent({ framework: \"ai-sdk\", storage: { driver: \"redis\", url: process.env.REDIS_URL } }) }\n */\nexport function agent(options: AgentOptions = {}) {\n const basePath = normalizeBasePath(options.basePath ?? DEFAULT_BASE_PATH);\n const config: AgentUnitConfig & { basePath: string } = { ...options, basePath };\n const instance: AgentInstance = { basePath, framework: options.framework };\n // agent-unit's engine reads it from the server that evaluates farm.config (see ./config).\n registerAgentUnitConfig(config);\n\n return defineIntegration({\n category: \"agent\",\n type: \"agent-unit\",\n instance,\n plugins: [createAgentPlugin(config)],\n });\n}\n","import type { AgentUnitConfig } from \"agent-unit\";\n\nconst AGENT_UNIT_CONFIG = Symbol.for(\"farm.agent.config\");\n\ntype ConfigHolder = { [AGENT_UNIT_CONFIG]?: AgentUnitConfig };\n\n/** @internal Called by `agent()` as farm.config evaluates, in Farm's process and in the server. */\nexport function registerAgentUnitConfig(config: AgentUnitConfig): void {\n (globalThis as ConfigHolder)[AGENT_UNIT_CONFIG] = config;\n}\n\n/**\n * The agent-unit config of the app's `agent()` integration. agent-unit's engine reads it when it is\n * first used: by then Farm's server has evaluated farm.config, so settings such as storage URLs and\n * `authorize` come from where the server runs.\n */\nexport default function agentUnitConfig(): AgentUnitConfig {\n const config = (globalThis as ConfigHolder)[AGENT_UNIT_CONFIG];\n if (!config) {\n throw new Error(\n \"@farm.js/agent: the server has not evaluated farm.config, so the agent() settings are missing. Mount agent() under `integrations` in farm.config.\",\n );\n }\n return config;\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport type { TLSSocket } from \"node:tls\";\nimport { fileURLToPath } from \"node:url\";\nimport { definePlugin } from \"@farm.js/core/plugin\";\nimport type { AgentUnitConfig } from \"agent-unit\";\nimport type { DevHandler } from \"agent-unit/build\";\nimport type { Plugin as VitePlugin } from \"vite\";\n\n// agent-unit's build and dev servers load only in Farm's own process. Farm bundles farm.config into\n// the production server, and a bundler follows literal imports, so these must not be literal.\nconst loadBuildOnly = <T>(specifier: string): Promise<T> => import(/* @vite-ignore */ specifier);\n\nexport function isUnder(pathname: string, basePath: string): boolean {\n return basePath === \"/\" || pathname === basePath || pathname.startsWith(`${basePath}/`);\n}\n\n/** Serves the agents in `farm dev` and builds them into the production server. */\nexport function createAgentPlugin(config: AgentUnitConfig & { basePath: string }) {\n const { basePath } = config;\n // Set by `configure`, in Farm's process: farm.config also evaluates inside the server, where a\n // Worker has no working directory.\n let root: string | undefined;\n let agentsDir = config.agentsDir ?? \"agents\";\n let devHandler: Promise<DevHandler> | undefined;\n\n const closeDev = async () => {\n const pending = devHandler;\n devHandler = undefined;\n await (await pending?.catch(() => undefined))?.close();\n };\n const buildConfig = (): AgentUnitConfig => ({ ...config, agentsDir });\n\n const devPlugin: VitePlugin = {\n name: \"farm:agent-dev\",\n apply: \"serve\",\n // Ahead of Farm's API middleware, which answers 404 for API paths it does not route.\n enforce: \"pre\",\n // Vite runs this again on every restart, while the agents keep running in one dev handler.\n configureServer(server) {\n devHandler ??= loadBuildOnly<typeof import(\"agent-unit/build\")>(\"agent-unit/build\").then(\n ({ createDevHandler }) =>\n createDevHandler({ root: root ?? process.cwd(), config: buildConfig() }),\n );\n const handler = devHandler;\n server.middlewares.use((req, res, next) => {\n if (!isUnder((req.url ?? \"/\").split(\"?\")[0]!, basePath)) return next();\n void serveDev(handler, req, res).catch(next);\n });\n },\n };\n\n return definePlugin({\n name: \"farm:agent\",\n configure(farmConfig, context) {\n root = path.resolve(farmConfig.root ?? process.cwd());\n agentsDir = config.agentsDir ?? path.posix.join(farmConfig.srcDir ?? \"src\", \"agents\");\n if (!context.isDev) return;\n const vite = farmConfig.vite ?? {};\n return { ...farmConfig, vite: { ...vite, plugins: [devPlugin, ...(vite.plugins ?? [])] } };\n },\n router: {\n // agent-unit answers every path under basePath, so an app route there would never be reached.\n discovered(route) {\n if (route.kind !== \"api\" || !isUnder(route.path, basePath)) return;\n throw new Error(\n `@farm.js/agent: ${route.filePath} is served at ${route.path}, which the agent API owns (basePath \"${basePath}\"). Move the route, or set another basePath.`,\n );\n },\n },\n build: {\n // agent-unit builds the agents into Farm's Nitro output for its preset: their framework\n // adapters, storage, the sweep and `tasks` as scheduled tasks.\n async configure(nitroConfig) {\n const { agentUnit } =\n await loadBuildOnly<typeof import(\"agent-unit/nitro\")>(\"agent-unit/nitro\");\n const module = agentUnit({\n root: root ?? process.cwd(),\n basePath,\n config: buildConfig(),\n // The module agent-unit's engine reads its config from in the built server.\n configFile: fileURLToPath(new URL(\"./config.js\", import.meta.url)),\n });\n return { ...nitroConfig, modules: [...(nitroConfig.modules ?? []), module] };\n },\n },\n dev: {\n server(_viteServer, { lifecycle }) {\n lifecycle.onShutdown(closeDev);\n },\n },\n runtime: {\n close: closeDev,\n },\n });\n}\n\nasync function serveDev(\n handler: Promise<DevHandler>,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const { sendWebResponse } =\n await loadBuildOnly<typeof import(\"@farm.js/core/server\")>(\"@farm.js/core/server\");\n const response = await (await handler).fetch(toWebRequest(req, res));\n await sendWebResponse(res, response);\n}\n\nfunction toWebRequest(req: IncomingMessage, res: ServerResponse): Request {\n const protocol = (req.socket as TLSSocket).encrypted ? \"https\" : \"http\";\n const url = new URL(req.url ?? \"/\", `${protocol}://${req.headers.host ?? \"localhost\"}`);\n const headers = new Headers();\n for (let index = 0; index < req.rawHeaders.length; index += 2) {\n const name = req.rawHeaders[index]!;\n if (!name.startsWith(\":\")) headers.append(name, req.rawHeaders[index + 1]!);\n }\n // A client that leaves an event stream ends the run's subscription, not the run.\n const controller = new AbortController();\n res.once(\"close\", () => {\n if (!res.writableFinished) controller.abort();\n });\n const method = req.method ?? \"GET\";\n const hasBody = method !== \"GET\" && method !== \"HEAD\";\n return new Request(url, {\n method,\n headers,\n signal: controller.signal,\n body: hasBody ? (Readable.toWeb(req) as ReadableStream<Uint8Array>) : undefined,\n ...(hasBody ? { duplex: \"half\" } : {}),\n } as RequestInit);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/plugin.ts"],"sourcesContent":["import { defineIntegration } from \"@farm.js/core\";\nimport type { AgentFramework, AgentUnitConfig } from \"agent-unit\";\n\nimport { registerAgentUnitConfig } from \"./config.js\";\nimport { createAgentPlugin } from \"./plugin.js\";\n\nexport type { AgentFramework };\n\n/**\n * agent-unit's settings, minus what Farm owns: the host (`deploy.target`), the server and the\n * Nitro and Workers settings, which belong in farm.config.\n */\nexport interface AgentOptions extends Omit<\n AgentUnitConfig,\n \"server\" | \"preset\" | \"runtime\" | \"cloudflare\" | \"nitro\"\n> {\n /** Where the agent API is mounted, under `/api`. It owns every path under it. Default `/api/agent`. */\n basePath?: string;\n /** The folder of agent files, relative to the app root. Default `agents` in `srcDir`. */\n agentsDir?: string;\n}\n\nexport interface AgentInstance {\n readonly basePath: string;\n readonly framework?: AgentFramework;\n}\n\nconst DEFAULT_BASE_PATH = \"/api/agent\";\n\nfunction normalizeBasePath(value: string): string {\n if (!value.startsWith(\"/\")) {\n throw new Error(`@farm.js/agent: basePath must start with \"/\", got \"${value}\".`);\n }\n const basePath = value.replace(/\\/+$/, \"\");\n // The agents are API routes, so Farm's middleware and guards run for them.\n if (basePath !== \"/api\" && !basePath.startsWith(\"/api/\")) {\n throw new Error(`@farm.js/agent: basePath must be under /api, got \"${value}\".`);\n }\n return basePath;\n}\n\n/**\n * Durable agents in a Farm app: the agents in `src/agents`, written with the AI SDK, Mastra,\n * LangGraph, the OpenAI Agents SDK or agent-unit's `defineAgent`, served under `basePath` in\n * `farm dev` and built by agent-unit into every deployment target.\n *\n * @example\n * integrations: { agent: agent({ framework: \"ai-sdk\", storage: { driver: \"redis\", url: process.env.REDIS_URL } }) }\n */\nexport function agent(options: AgentOptions = {}) {\n const basePath = normalizeBasePath(options.basePath ?? DEFAULT_BASE_PATH);\n const config: AgentUnitConfig & { basePath: string } = { ...options, basePath };\n const instance: AgentInstance = { basePath, framework: options.framework };\n // agent-unit's engine reads it from the server that evaluates farm.config (see ./config).\n registerAgentUnitConfig(config);\n\n return defineIntegration({\n category: \"agent\",\n type: \"agent-unit\",\n instance,\n plugins: [createAgentPlugin(config)],\n });\n}\n","import type { AgentUnitConfig } from \"agent-unit\";\n\nconst AGENT_UNIT_CONFIG = Symbol.for(\"farm.agent.config\");\n\ntype ConfigHolder = { [AGENT_UNIT_CONFIG]?: AgentUnitConfig };\n\n/** @internal Called by `agent()` as farm.config evaluates, in Farm's process and in the server. */\nexport function registerAgentUnitConfig(config: AgentUnitConfig): void {\n (globalThis as ConfigHolder)[AGENT_UNIT_CONFIG] = config;\n}\n\n/**\n * The agent-unit config of the app's `agent()` integration. agent-unit's engine reads it when it is\n * first used: by then Farm's server has evaluated farm.config, so settings such as storage URLs and\n * `authorize` come from where the server runs.\n */\nexport default function agentUnitConfig(): AgentUnitConfig {\n const config = (globalThis as ConfigHolder)[AGENT_UNIT_CONFIG];\n if (!config) {\n throw new Error(\n \"@farm.js/agent: the server has not evaluated farm.config, so the agent() settings are missing. Mount agent() under `integrations` in farm.config.\",\n );\n }\n return config;\n}\n","import { writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { definePlugin } from \"@farm.js/core/plugin\";\nimport type { AgentUnitConfig } from \"agent-unit\";\nimport type { DevHandler } from \"agent-unit/build\";\nimport type { agentUnit } from \"agent-unit/nitro\";\n\ntype Nitro = Parameters<NonNullable<ReturnType<typeof agentUnit>[\"setup\"]>>[0];\n\n// agent-unit's build and dev servers load only in Farm's own process. Farm bundles farm.config into\n// the production server, and a bundler follows literal imports, so these must not be literal.\nconst loadBuildOnly = <T>(specifier: string): Promise<T> => import(/* @vite-ignore */ specifier);\n\n/** Set by the built server's agent module (see SERVER_MODULE): agent-unit's engine. */\nconst AGENT_SERVER = Symbol.for(\"farm.agent.server\");\ntype AgentServer = { handle(request: Request): Promise<Response> };\ntype ServerHolder = { [AGENT_SERVER]?: AgentServer };\n\n// Every method agent-unit answers: the run API, MCP and the internal continue and sweep endpoints.\nconst METHODS = [\"get\", \"post\", \"delete\"] as const;\n\nexport function isUnder(pathname: string, basePath: string): boolean {\n return basePath === \"/\" || pathname === basePath || pathname.startsWith(`${basePath}/`);\n}\n\n// The built server's agent module, next to agent-unit's runtime. It hands the engine to the routes\n// below, and as Nitro middleware on the agent paths it passes on the host's waitUntil, which only\n// hosts that keep work alive after the response have, as agent-unit's own entry does.\nconst SERVER_MODULE = `// Generated by @farm.js/agent. Do not edit.\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { getUnit } from \"./runtime.mjs\";\n\nconst hostWaitUntil = new AsyncLocalStorage();\n\nglobalThis[Symbol.for(\"farm.agent.server\")] = {\n handle(request) {\n const waitUntil = hostWaitUntil.getStore();\n return getUnit().handler(request, waitUntil ? { waitUntil } : {});\n },\n};\n\nexport default function farmAgentHost(event, next) {\n const req = event.req;\n return typeof req.waitUntil === \"function\"\n ? hostWaitUntil.run((promise) => req.waitUntil(promise), next)\n : next();\n}\n`;\n\n// Nitro's Vercel preset runs scheduled tasks from a public cron route that checks CRON_SECRET only\n// when it is set. As Nitro middleware on that route, this module rejects every call without the\n// secret, the way Farm's own cron routes do.\nconst CRON_GUARD_MODULE = `// Generated by @farm.js/agent. Do not edit.\nimport { isCronRequestAuthorized } from \"@farm.js/core/cron\";\n\nexport default function farmAgentCronGuard(event, next) {\n if (isCronRequestAuthorized(event.req)) return next();\n return Response.json({ error: \"Unauthorized cron request.\" }, { status: 401 });\n}\n`;\n\n/** Guards Nitro's Vercel cron route when agent-unit has scheduled the sweep or the app's tasks. */\nfunction guardVercelCron(nitro: Nitro, dir: string): void {\n const preset = String(nitro.options.preset ?? \"\");\n if (!preset.startsWith(\"vercel\")) return;\n if (!nitro.options.experimental?.tasks) return;\n if (Object.keys(nitro.options.scheduledTasks ?? {}).length === 0) return;\n\n const route = nitro.options.vercel?.cronHandlerRoute || \"/_vercel/cron\";\n const guard = path.posix.join(dir, \"farm-cron-guard.mjs\");\n writeFileSync(guard, CRON_GUARD_MODULE);\n nitro.options.handlers.unshift(\n { route, handler: guard, middleware: true, lazy: false },\n { route: `${route}/**`, handler: guard, middleware: true, lazy: false },\n );\n if (!process.env.CRON_SECRET) {\n nitro.logger.warn(\n `@farm.js/agent: Vercel Cron runs the agent sweep and tasks at ${route}, which rejects every call without CRON_SECRET. Set CRON_SECRET in the Vercel project's environment variables, or the scheduled runs answer 401.`,\n );\n }\n}\n\n/**\n * Serves the agents in `farm dev` and builds them into the production server, as API routes under\n * `basePath`: Farm's middleware, plugin guards and integration guards run for them as for any route.\n */\nexport function createAgentPlugin(config: AgentUnitConfig & { basePath: string }) {\n const { basePath } = config;\n // Set by `configure`, in Farm's process: farm.config also evaluates inside the server, where a\n // Worker has no working directory.\n let root: string | undefined;\n let agentsDir = config.agentsDir ?? \"agents\";\n let isDev = false;\n let devHandler: Promise<DevHandler> | undefined;\n\n const buildConfig = (): AgentUnitConfig => ({ ...config, agentsDir });\n const startDev = () =>\n (devHandler ??= loadBuildOnly<typeof import(\"agent-unit/build\")>(\"agent-unit/build\").then(\n ({ createDevHandler }) =>\n createDevHandler({ root: root ?? process.cwd(), config: buildConfig() }),\n ));\n const closeDev = async () => {\n const pending = devHandler;\n devHandler = undefined;\n await (await pending?.catch(() => undefined))?.close();\n };\n\n const handle = async (request: Request): Promise<Response> => {\n // A dev server runs the agents from source, kept across Vite restarts.\n if (isDev) return (await startDev()).fetch(request);\n const server = (globalThis as ServerHolder)[AGENT_SERVER];\n if (!server) {\n throw new Error(\n \"@farm.js/agent: this server was built without the agents. Build it with `farm build`, which adds them.\",\n );\n }\n return server.handle(request);\n };\n\n return definePlugin({\n name: \"farm:agent\",\n configure(farmConfig, context) {\n root = path.resolve(farmConfig.root ?? process.cwd());\n agentsDir = config.agentsDir ?? path.posix.join(farmConfig.srcDir ?? \"src\", \"agents\");\n isDev = context.isDev;\n },\n // Ordinary API routes, so a request passes Farm's middleware, runtime.before and integration\n // guards before it reaches the agents, in development and in every production target.\n routes: ({ route }) =>\n METHODS.flatMap((method) =>\n [basePath, `${basePath}/[...path]`].map((pattern) =>\n route[method](pattern, { handler: (request) => handle(request) }),\n ),\n ),\n router: {\n // agent-unit answers every path under basePath, so an app route there would never be reached.\n discovered(route) {\n if (route.kind !== \"api\" || !route.filePath || !isUnder(route.path, basePath)) return;\n throw new Error(\n `@farm.js/agent: ${route.filePath} is served at ${route.path}, which the agent API owns (basePath \"${basePath}\"). Move the route, or set another basePath.`,\n );\n },\n },\n build: {\n // agent-unit builds the agents into Farm's Nitro output for its preset: their framework\n // adapters, storage, the sweep and `tasks` as scheduled tasks. Its own handler for basePath\n // would answer before Farm's, so Farm's routes serve the API instead.\n async configure(nitroConfig) {\n const { agentUnit } =\n await loadBuildOnly<typeof import(\"agent-unit/nitro\")>(\"agent-unit/nitro\");\n const unit = agentUnit({\n root: root ?? process.cwd(),\n basePath,\n config: buildConfig(),\n // The module agent-unit's engine reads its config from in the built server.\n configFile: fileURLToPath(new URL(\"./config.js\", import.meta.url)),\n });\n const module: typeof unit = {\n name: unit.name,\n async setup(nitro) {\n const before = nitro.options.handlers.length;\n await unit.setup!(nitro);\n const added = nitro.options.handlers.splice(before);\n const entry = added.find((handler) => handler.handler.endsWith(\"entry.mjs\"))?.handler;\n if (!entry) {\n throw new Error(\n \"@farm.js/agent: agent-unit's Nitro module did not mount its handler. Install the agent-unit version @farm.js/agent supports.\",\n );\n }\n nitro.options.handlers.push(...added.filter((handler) => handler.handler !== entry));\n // Nitro writes handler paths into generated imports, so keep them in forward-slash\n // form; a Windows `D:\\…` path would lose its backslashes there.\n const server = path.posix.join(\n path.posix.dirname(entry.replaceAll(\"\\\\\", \"/\")),\n \"farm-server.mjs\",\n );\n writeFileSync(server, SERVER_MODULE);\n guardVercelCron(nitro, path.posix.dirname(server));\n nitro.options.handlers.push(\n { route: basePath, handler: server, middleware: true, lazy: false },\n { route: `${basePath}/**`, handler: server, middleware: true, lazy: false },\n );\n },\n };\n return { ...nitroConfig, modules: [...(nitroConfig.modules ?? []), module] };\n },\n },\n dev: {\n server(_viteServer, { lifecycle }) {\n void startDev().catch(() => {});\n lifecycle.onShutdown(closeDev);\n },\n },\n runtime: {\n close: closeDev,\n },\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/agent",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Durable agents for Farm.js, served and built by agent-unit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-unit",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"typescript": "^5.3.3",
|
|
45
45
|
"vite": "8.3.4",
|
|
46
46
|
"vitest": "5.0.5",
|
|
47
|
-
"@farm.js/core": "0.1.
|
|
47
|
+
"@farm.js/core": "0.1.51-beta.2"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@farm.js/core": ">=0.1.
|
|
50
|
+
"@farm.js/core": ">=0.1.50 <0.2.0"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=22.13.0"
|