@fevex/core 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +1110 -52
- package/dist/agents/index.d.mts +2 -23
- package/dist/agents/index.mjs +1 -1
- package/dist/artifacts/index.d.mts +2 -0
- package/dist/artifacts/index.mjs +2 -0
- package/dist/artifacts-gUzemET7.mjs +440 -0
- package/dist/channels/index.d.mts +2 -0
- package/dist/channels/index.mjs +192 -0
- package/dist/evals/index.d.mts +153 -0
- package/dist/evals/index.mjs +422 -0
- package/dist/fevex-_KOsOsB8.d.mts +129 -0
- package/dist/http/index.d.mts +150 -0
- package/dist/http/index.mjs +692 -0
- package/dist/index-26ilbzAs.d.mts +50 -0
- package/dist/index-B8EcvWeN.d.mts +103 -0
- package/dist/index-BEqYWean.d.mts +118 -0
- package/dist/index-Btth7rdL.d.mts +58 -0
- package/dist/index-CFzrvDQ0.d.mts +21 -0
- package/dist/index-CIHWc5V1.d.mts +116 -0
- package/dist/index-D7r0puBi.d.mts +64 -0
- package/dist/index-DHcfIfh-.d.mts +224 -0
- package/dist/index-DUhNa7IR.d.mts +73 -0
- package/dist/index-lCCFokc6.d.mts +617 -0
- package/dist/index.d.mts +39 -15
- package/dist/index.mjs +4962 -208
- package/dist/knowledge/index.d.mts +2 -0
- package/dist/knowledge/index.mjs +289 -0
- package/dist/model-stream-JQdHDDcj.mjs +131 -0
- package/dist/models/index.d.mts +2 -27
- package/dist/observability/index.d.mts +2 -0
- package/dist/observability/index.mjs +1 -0
- package/dist/policies/index.d.mts +2 -0
- package/dist/policies/index.mjs +6 -0
- package/dist/run-error-Cr17wPGH.mjs +23 -0
- package/dist/run-support-CmZ6Xo6H.mjs +162 -0
- package/dist/runtime/index.d.mts +2 -17
- package/dist/runtime/index.mjs +354 -1
- package/dist/sandbox/index.d.mts +2 -0
- package/dist/sandbox/index.mjs +147 -0
- package/dist/teams/index.d.mts +2 -0
- package/dist/teams/index.mjs +2 -0
- package/dist/teams-Ctz676G4.mjs +82 -0
- package/dist/testing/index.d.mts +40 -8
- package/dist/testing/index.mjs +668 -23
- package/dist/tools/index.d.mts +3 -31
- package/dist/tools/index.mjs +442 -2
- package/dist/types-CckDlhYo.d.mts +575 -0
- package/dist/workflows/index.d.mts +2 -0
- package/dist/workflows/index.mjs +6 -0
- package/package.json +43 -3
- package/dist/exact-definition-NHBBFlFR.d.mts +0 -4
- package/dist/index-D_RBJmVt.d.mts +0 -44
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ TypeScript.
|
|
|
6
6
|
Fevex gives you the small contracts needed to define agents, tools, model
|
|
7
7
|
adapters, validation and tests without adopting a heavy runtime.
|
|
8
8
|
|
|
9
|
-
> **Status:** early MVP. Fevex is usable for local
|
|
9
|
+
> **Status:** early MVP. Fevex is usable for bounded local agent runs, but
|
|
10
10
|
> its API may change before the first stable release.
|
|
11
11
|
|
|
12
12
|
## Why Fevex
|
|
@@ -17,23 +17,38 @@ adapters, validation and tests without adopting a heavy runtime.
|
|
|
17
17
|
runtime boundary.
|
|
18
18
|
- **Small runtime:** one package, ESM, Node.js 20+, no database or service
|
|
19
19
|
required.
|
|
20
|
-
- **Observable:** successful and failed runs emit structured events
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
- **Observable:** successful and failed runs emit structured events and optional
|
|
21
|
+
local traces.
|
|
22
|
+
- **Testable:** `@fevex/core/testing` includes a deterministic model and shared
|
|
23
|
+
model contract checks; `@fevex/core/evals` adds local regression suites.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
npm install fevex
|
|
28
|
+
npm install @fevex/core
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
The npm release pipeline publishes exactly three official packages:
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
|
-
npm install fevex @fevex/openai
|
|
34
|
-
npm install fevex @fevex/deepseek
|
|
34
|
+
npm install @fevex/core @fevex/openai
|
|
35
|
+
npm install @fevex/core @fevex/deepseek
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
CI builds, typechecks, tests and tarball-smokes all ten packages. The
|
|
39
|
+
repository-only integrations below are **not published to npm** by the release
|
|
40
|
+
pipeline; use them from this repository until they are published:
|
|
41
|
+
|
|
42
|
+
| Package | Purpose |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `@fevex/mcp` | Model Context Protocol tools and connections |
|
|
45
|
+
| `@fevex/openapi` | Tools generated from an OpenAPI document |
|
|
46
|
+
| `@fevex/sqlite` | SQLite run and session store |
|
|
47
|
+
| `@fevex/postgres` | PostgreSQL run and session store |
|
|
48
|
+
| `@fevex/opentelemetry` | OpenTelemetry trace and metric export |
|
|
49
|
+
| `@fevex/browser` | Browser automation tools |
|
|
50
|
+
| `@fevex/sandbox-docker` | Docker-backed production sandbox provider |
|
|
51
|
+
|
|
37
52
|
Schemas are optional. The examples use Zod 4, but any Standard
|
|
38
53
|
Schema-compatible validator can be used:
|
|
39
54
|
|
|
@@ -41,13 +56,27 @@ Schema-compatible validator can be used:
|
|
|
41
56
|
npm install zod
|
|
42
57
|
```
|
|
43
58
|
|
|
59
|
+
## Compatibility & releases
|
|
60
|
+
|
|
61
|
+
FEVEX is pre-1.0. Documented imports from official packages are the public API;
|
|
62
|
+
`src/internal`, undocumented paths and checkpoint shapes are not. While in alpha,
|
|
63
|
+
`minor` releases may contain breaking changes with migration notes; `patch`
|
|
64
|
+
releases stay compatible. Store migrations are forward-only, checkpoints are
|
|
65
|
+
versioned and private, and downgrades are not supported.
|
|
66
|
+
|
|
67
|
+
Only the latest alpha is supported; pin exact versions in production. Releases
|
|
68
|
+
come from reviewed changesets, publish only the three official packages, and
|
|
69
|
+
place alpha versions on the npm `latest` tag. Read the full
|
|
70
|
+
[compatibility and release policy](https://fevex.dev/docs/compatibility/) before
|
|
71
|
+
upgrading a durable deployment.
|
|
72
|
+
|
|
44
73
|
## Quickstart
|
|
45
74
|
|
|
46
75
|
This example executes a complete model-tool-model cycle without network access:
|
|
47
76
|
|
|
48
77
|
```ts
|
|
49
|
-
import { createFevex, defineAgent, defineTool } from 'fevex';
|
|
50
|
-
import { fakeModel } from 'fevex/testing';
|
|
78
|
+
import { createFevex, defineAgent, defineTool } from '@fevex/core';
|
|
79
|
+
import { fakeModel } from '@fevex/core/testing';
|
|
51
80
|
import { z } from 'zod';
|
|
52
81
|
|
|
53
82
|
const accountInput = z.object({
|
|
@@ -60,7 +89,7 @@ const accountOutput = z.object({
|
|
|
60
89
|
});
|
|
61
90
|
|
|
62
91
|
const getAccount = defineTool({
|
|
63
|
-
name: '
|
|
92
|
+
name: 'accounts_get',
|
|
64
93
|
description: 'Get an account by ID.',
|
|
65
94
|
inputSchema: accountInput,
|
|
66
95
|
outputSchema: accountOutput,
|
|
@@ -73,11 +102,11 @@ const model = fakeModel(
|
|
|
73
102
|
{
|
|
74
103
|
toolCalls: [{
|
|
75
104
|
id: 'call-1',
|
|
76
|
-
name: '
|
|
105
|
+
name: 'accounts_get',
|
|
77
106
|
input: { accountId: 42 },
|
|
78
107
|
}],
|
|
79
108
|
},
|
|
80
|
-
{ output: 'Account 42 is active.' },
|
|
109
|
+
{ output: { answer: 'Account 42 is active.' } },
|
|
81
110
|
);
|
|
82
111
|
|
|
83
112
|
const app = createFevex({
|
|
@@ -85,48 +114,188 @@ const app = createFevex({
|
|
|
85
114
|
agents: [defineAgent({
|
|
86
115
|
name: 'support',
|
|
87
116
|
instructions: 'Answer account questions clearly.',
|
|
88
|
-
tools: ['
|
|
89
|
-
outputSchema: z.string(),
|
|
117
|
+
tools: ['accounts_get'],
|
|
118
|
+
outputSchema: z.object({ answer: z.string() }),
|
|
90
119
|
})],
|
|
91
120
|
tools: [getAccount],
|
|
92
121
|
});
|
|
93
122
|
|
|
94
|
-
const result = await app.runAgent<string, string>('support', {
|
|
123
|
+
const result = await app.runAgent<string, { answer: string }>('support', {
|
|
95
124
|
input: 'What is the status of account 42?',
|
|
96
125
|
});
|
|
97
126
|
|
|
98
|
-
console.log(result.output);
|
|
127
|
+
console.log(result.output.answer);
|
|
99
128
|
// Account 42 is active.
|
|
100
129
|
```
|
|
101
130
|
|
|
102
131
|
`fakeModel` returns its configured responses in order and records every input in
|
|
103
132
|
`model.calls`.
|
|
104
133
|
|
|
134
|
+
## Durable workflows
|
|
135
|
+
|
|
136
|
+
Workflows require a `DurableRunStore`. Their definition owns the input, output,
|
|
137
|
+
event and limit contracts:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const review = defineWorkflow({
|
|
141
|
+
name: 'review',
|
|
142
|
+
version: '2',
|
|
143
|
+
inputSchema: z.object({ draft: z.string() }),
|
|
144
|
+
outputSchema: z.string(),
|
|
145
|
+
events: {
|
|
146
|
+
'review.approved': {
|
|
147
|
+
payloadSchema: z.object({ approved: z.literal(true) }),
|
|
148
|
+
requireActor: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
limits: { maxSteps: 8, maxToolCalls: 12 },
|
|
152
|
+
async run(step, input) {
|
|
153
|
+
const draft = await step.agent('draft', 'writer', { input });
|
|
154
|
+
const approval = await step.waitForEvent('approval', 'review.approved');
|
|
155
|
+
return `${draft.output} (${approval.actor?.id}, ${approval.receivedAt})`;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Input is validated and transformed before the initial checkpoint is stored.
|
|
161
|
+
Output is always validated with the definition schema, including after a pause
|
|
162
|
+
or recovery. Effective workflow limits combine definition and request limits;
|
|
163
|
+
agent steps additionally combine their own agent/request limits. These values
|
|
164
|
+
are checkpointed so recovery cannot silently widen a budget.
|
|
165
|
+
|
|
166
|
+
`stepId` is the durable identity of a step: keep it stable while its meaning is
|
|
167
|
+
stable. Increment `version` when replay order, business rules or step meanings
|
|
168
|
+
change. Completed agent steps are reused on replay and `step.agent` always
|
|
169
|
+
returns `{ runId, sessionId, output, usage? }`, without a transient `events`
|
|
170
|
+
field.
|
|
171
|
+
|
|
172
|
+
Child execution context inherits the workflow context. Namespace, attributes
|
|
173
|
+
and prompt values are merged; an authenticated parent actor cannot be replaced
|
|
174
|
+
by a child. `parallel` waits for every declared task to settle, records durable
|
|
175
|
+
child successes, reports multiple failures in declaration order and resolves
|
|
176
|
+
multiple pauses one at a time. Arbitrary promises used in `parallel` must be
|
|
177
|
+
pure or replay-safe; side effects belong in durable agent/tool steps.
|
|
178
|
+
|
|
179
|
+
External events must be declared. `waitForEvent` returns
|
|
180
|
+
`{ payload, actor, receivedAt }`; payload and required actor are checked before
|
|
181
|
+
the checkpoint changes. Cancelling a workflow first cancels its running or
|
|
182
|
+
paused children. Compensation runs in reverse completed-step order; if both the
|
|
183
|
+
workflow and compensation fail, the terminal error is a stable
|
|
184
|
+
`AggregateError`.
|
|
185
|
+
|
|
186
|
+
Durable executions start with a checkpoint v2 and lease in the same atomic
|
|
187
|
+
store operation. An external worker may recover an orphaned `running` run after
|
|
188
|
+
its lease expires:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
await app.recoverRun(runId, { actor: { id: 'recovery-worker', type: 'service' } });
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Paused runs use `resumeRun`; terminal runs and older checkpoints are rejected.
|
|
195
|
+
Fevex does not poll globally: deployments need an external orphan detector and
|
|
196
|
+
a scheduler that calls `resumeRun` for elapsed timers. Exactly-once protection
|
|
197
|
+
is limited to keyed/idempotent tools and durable steps.
|
|
198
|
+
|
|
199
|
+
## Advanced teams
|
|
200
|
+
|
|
201
|
+
Teams are an opt-in coordination layer on the durable workflow engine. They
|
|
202
|
+
reuse registered agents and cannot widen an agent's model, tools, policies,
|
|
203
|
+
sandbox or limits:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const model = fakeModel(
|
|
207
|
+
{ output: 'plan' },
|
|
208
|
+
{ output: 'research' },
|
|
209
|
+
{ output: 'code' },
|
|
210
|
+
{ output: 'approved' },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const softwareTeam = defineTeam({
|
|
214
|
+
name: 'software-team',
|
|
215
|
+
version: '1',
|
|
216
|
+
supervisor: 'planner',
|
|
217
|
+
members: [
|
|
218
|
+
{ agent: 'researcher', role: 'research' },
|
|
219
|
+
{ agent: 'coder', role: 'implementation' },
|
|
220
|
+
{ agent: 'reviewer', role: 'review' },
|
|
221
|
+
],
|
|
222
|
+
limits: { maxDelegations: 12, maxParallel: 2 },
|
|
223
|
+
async run(team, input) {
|
|
224
|
+
const plan = await team.delegate('plan', {
|
|
225
|
+
agent: 'planner',
|
|
226
|
+
task: input,
|
|
227
|
+
expectedOutput: 'Implementation plan',
|
|
228
|
+
});
|
|
229
|
+
const work = await team.parallel('work', {
|
|
230
|
+
research: () =>
|
|
231
|
+
team.delegate('research', { agent: 'researcher', task: plan.output }),
|
|
232
|
+
implementation: () =>
|
|
233
|
+
team.delegate('implementation', { agent: 'coder', task: plan.output }),
|
|
234
|
+
});
|
|
235
|
+
return (
|
|
236
|
+
await team.handoff('review', {
|
|
237
|
+
from: 'coder',
|
|
238
|
+
to: 'reviewer',
|
|
239
|
+
reason: 'Final review',
|
|
240
|
+
task: work,
|
|
241
|
+
})
|
|
242
|
+
).output;
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const app = createFevex({ models, agents, teams: [softwareTeam] });
|
|
247
|
+
const result = await app.runTeam('software-team', { input: 'Implement the change.' });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Delegation and parallel step IDs are durable identities and must remain stable
|
|
251
|
+
within a team version. Team runs emit `team.*` events and relay child events
|
|
252
|
+
with `teamDelegationId` and `teamAgentName`. `startTeam`, `streamTeam`,
|
|
253
|
+
`getTeamRun`, `resumeRun`, `recoverRun` and `cancelRun` use the same durable
|
|
254
|
+
store as agents and workflows.
|
|
255
|
+
|
|
256
|
+
This release intentionally omits automatic routers, network/graph modes,
|
|
257
|
+
quorum, shared blackboards and nested teams. Coordination remains explicit
|
|
258
|
+
TypeScript and shared context travels through task inputs, outputs and the
|
|
259
|
+
existing memory contracts.
|
|
260
|
+
|
|
105
261
|
## Connect A Model
|
|
106
262
|
|
|
107
263
|
Real providers connect through the small `ModelGateway` contract:
|
|
108
264
|
|
|
109
265
|
```ts
|
|
110
|
-
import type { ModelGateway } from 'fevex/models';
|
|
266
|
+
import type { ModelGateway } from '@fevex/core/models';
|
|
111
267
|
|
|
112
268
|
export const model: ModelGateway = {
|
|
113
|
-
async
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
269
|
+
async *stream(input) {
|
|
270
|
+
for await (const event of yourProvider.stream(input)) {
|
|
271
|
+
if (event.type === 'text.delta') {
|
|
272
|
+
yield { type: 'output.delta', delta: event.text };
|
|
273
|
+
} else if (event.type === 'completed') {
|
|
274
|
+
yield {
|
|
275
|
+
type: 'completed',
|
|
276
|
+
result: {
|
|
277
|
+
output: event.output,
|
|
278
|
+
toolCalls: event.toolCalls,
|
|
279
|
+
usage: event.usage,
|
|
280
|
+
providerState: event.providerState,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
126
285
|
},
|
|
127
286
|
};
|
|
128
287
|
```
|
|
129
288
|
|
|
289
|
+
The stream must emit visible text or JSON fragments as `output.delta` followed
|
|
290
|
+
by exactly one `completed` event. Fevex rejects missing or duplicate terminals,
|
|
291
|
+
post-terminal events and a final output that does not match the accumulated
|
|
292
|
+
deltas.
|
|
293
|
+
|
|
294
|
+
`providerState` is opaque, provider-specific continuation data. Fevex carries
|
|
295
|
+
it between model steps within one run and never adds it to agent messages,
|
|
296
|
+
events or `RunResult`. A direct `ModelGateway` consumer must pass it unchanged
|
|
297
|
+
to the next call; it is ephemeral and should not be inspected or persisted.
|
|
298
|
+
|
|
130
299
|
Provider integrations remain outside the core package. The first official
|
|
131
300
|
adapter is `@fevex/openai`, authored by Fevex over the native OpenAI HTTP API:
|
|
132
301
|
|
|
@@ -147,39 +316,94 @@ import { createDeepSeek } from '@fevex/deepseek';
|
|
|
147
316
|
|
|
148
317
|
const model = createDeepSeek({
|
|
149
318
|
apiKey: process.env.DEEPSEEK_API_KEY!,
|
|
150
|
-
|
|
319
|
+
schemaPolicy: 'best-effort',
|
|
320
|
+
})('deepseek-v4-flash');
|
|
151
321
|
```
|
|
152
322
|
|
|
153
323
|
Applications can still implement `ModelGateway` directly as shown above.
|
|
154
324
|
|
|
325
|
+
Official adapters preserve ordinary model text exactly. JSON-looking text is
|
|
326
|
+
still a string unless `outputSchema` is present; structured output is then
|
|
327
|
+
parsed as JSON and validated locally by Fevex. Tool names must match
|
|
328
|
+
`[A-Za-z0-9_-]{1,64}`.
|
|
329
|
+
|
|
330
|
+
`modelOptions` remains a provider escape hatch, but Fevex-owned request fields
|
|
331
|
+
take precedence: model, messages, tools, tool choice, schema format,
|
|
332
|
+
parallelism and preventive token limits cannot be replaced. Compatible options
|
|
333
|
+
such as temperature, metadata and provider-only reasoning settings are kept.
|
|
334
|
+
|
|
155
335
|
## Validation
|
|
156
336
|
|
|
337
|
+
`createFevex` validates the complete composition before returning the app,
|
|
338
|
+
including JavaScript values that bypass TypeScript. Invalid registrations throw
|
|
339
|
+
`FevexConfigurationError` with a stable `code`, while preserving a readable
|
|
340
|
+
message.
|
|
341
|
+
|
|
342
|
+
Codes follow one rule: `INVALID_CONFIG` covers the shape of the root config
|
|
343
|
+
object, and every registered entity gets its own code.
|
|
344
|
+
|
|
345
|
+
| Entity | Invalid | Duplicated by name | Referenced but missing |
|
|
346
|
+
|---|---|---|---|
|
|
347
|
+
| Model | `INVALID_MODEL` | — | `MISSING_MODEL` |
|
|
348
|
+
| Agent | `INVALID_AGENT` | `DUPLICATE_AGENT` | — |
|
|
349
|
+
| Tool | `INVALID_TOOL` | `DUPLICATE_TOOL` | `MISSING_TOOL` |
|
|
350
|
+
| Workflow | `INVALID_WORKFLOW` | `DUPLICATE_WORKFLOW` | — |
|
|
351
|
+
| Team | `INVALID_TEAM` | `DUPLICATE_TEAM` | — |
|
|
352
|
+
| Context provider | `INVALID_CONTEXT_PROVIDER` | `DUPLICATE_CONTEXT_PROVIDER` | — |
|
|
353
|
+
| Connection | `INVALID_CONNECTION` | — | — |
|
|
354
|
+
| Policy | `INVALID_POLICY` | — | — |
|
|
355
|
+
|
|
356
|
+
Connections have no duplicate code of their own: two connections sharing a name
|
|
357
|
+
produce colliding `<connection>__<tool>` entries and fail with
|
|
358
|
+
`DUPLICATE_TOOL`.
|
|
359
|
+
|
|
360
|
+
The app captures a shallow snapshot of registered definitions, tool lists,
|
|
361
|
+
limits, model options and `onEvent`. Mutating those caller-owned containers
|
|
362
|
+
after `createFevex` does not reconfigure a running app. Model gateways, schemas,
|
|
363
|
+
tool functions and nested provider-option values remain cooperative runtime
|
|
364
|
+
references and are not frozen.
|
|
365
|
+
|
|
157
366
|
Fevex accepts
|
|
158
367
|
[`StandardSchemaV1`](https://github.com/standard-schema/standard-schema)
|
|
159
368
|
validators for:
|
|
160
369
|
|
|
370
|
+
- agent, workflow and team input before it is persisted;
|
|
161
371
|
- tool input before `execute`;
|
|
162
372
|
- tool output before it is returned to the model;
|
|
163
|
-
- final agent output before it is returned to the application
|
|
373
|
+
- final agent, workflow and team output before it is returned to the application;
|
|
374
|
+
- declared workflow event payloads before a wait is resolved.
|
|
164
375
|
|
|
165
376
|
Schemas may transform values. The transformed result must still be JSON-safe.
|
|
166
|
-
|
|
167
|
-
|
|
377
|
+
Schemas belong to agent, workflow or team definitions; requests cannot replace an output
|
|
378
|
+
contract. Plain JSON Schema objects are not executable validators and are not
|
|
168
379
|
accepted by these fields.
|
|
169
380
|
|
|
170
381
|
When a model adapter needs schemas, Fevex converts authoring schemas that also
|
|
171
382
|
implement Standard JSON Schema into JSON Schema 2020-12 before calling
|
|
172
|
-
`ModelGateway.
|
|
173
|
-
`ToolSpec.inputSchema` and `
|
|
383
|
+
`ModelGateway.stream`. Adapters receive transportable JSON Schema in
|
|
384
|
+
`ToolSpec.inputSchema` and `ModelInput.outputSchema`; they never receive
|
|
174
385
|
the application's executable validators. If a run needs a schema at the model
|
|
175
386
|
boundary and the schema cannot be converted, Fevex fails before invoking the
|
|
176
387
|
model with `SCHEMA_NOT_TRANSPORTABLE`.
|
|
177
388
|
|
|
389
|
+
Transportable does not mean supported by every provider. Official adapters use
|
|
390
|
+
`schemaPolicy: "strict"` by default and validate their provider's documented
|
|
391
|
+
subset before making an HTTP request. Unsupported schemas fail with
|
|
392
|
+
`PROVIDER_SCHEMA_UNSUPPORTED`. Use `schemaPolicy: "best-effort"` explicitly
|
|
393
|
+
when local validation is sufficient and provider-level schema adherence is not
|
|
394
|
+
required; invalid model output can still fail locally and may require a retry
|
|
395
|
+
owned by the caller.
|
|
396
|
+
|
|
397
|
+
| Boundary | OpenAI strict | DeepSeek strict | Best effort |
|
|
398
|
+
| --- | --- | --- | --- |
|
|
399
|
+
| Tool input | Structured Outputs subset | Strict tools subset on `/beta` | Forwarded without provider guarantee |
|
|
400
|
+
| Final output | Structured Outputs subset | Unsupported | JSON instruction plus local validation |
|
|
401
|
+
|
|
178
402
|
Use `testModelGateway` to verify a custom model adapter against the shared
|
|
179
403
|
contract:
|
|
180
404
|
|
|
181
405
|
```ts
|
|
182
|
-
import { testModelGateway } from 'fevex/testing';
|
|
406
|
+
import { testModelGateway } from '@fevex/core/testing';
|
|
183
407
|
|
|
184
408
|
await testModelGateway(model);
|
|
185
409
|
```
|
|
@@ -204,42 +428,876 @@ The MVP emits:
|
|
|
204
428
|
|
|
205
429
|
```text
|
|
206
430
|
run.started
|
|
431
|
+
model.started
|
|
432
|
+
model.output.delta
|
|
207
433
|
model.completed
|
|
434
|
+
context.compacted
|
|
435
|
+
tool.started
|
|
208
436
|
tool.completed
|
|
209
437
|
tool.failed
|
|
210
438
|
run.completed
|
|
211
439
|
run.failed
|
|
440
|
+
run.cancelled
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`streamAgent` yields each persisted event as it happens. Use
|
|
444
|
+
`model.output.delta` to render output incrementally:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
for await (const event of app.streamAgent('support', { input: 'Hello' })) {
|
|
448
|
+
if (event.type === 'model.output.delta') {
|
|
449
|
+
process.stdout.write(event.payload.delta);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
212
452
|
```
|
|
213
453
|
|
|
214
454
|
Successful runs also return their events in `RunResult.events`.
|
|
455
|
+
Every event has a unique `id` and a run-local `sequence` starting at `1`.
|
|
456
|
+
`model.completed` includes the current step and accumulated usage.
|
|
457
|
+
Tool events include the current step, tool call ID and tool name.
|
|
458
|
+
`run.completed` includes the final JSON output and accumulated usage.
|
|
459
|
+
|
|
460
|
+
`onEvent` is a best-effort observer isolated from execution. Its exceptions do
|
|
461
|
+
not fail the run, and it continues receiving later events. Fevex does not await
|
|
462
|
+
a Promise returned by the observer, but absorbs its rejection to avoid an
|
|
463
|
+
unhandled rejection.
|
|
464
|
+
|
|
465
|
+
Consume the same execution incrementally with `streamAgent`:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
for await (const event of app.streamAgent('support', {
|
|
469
|
+
input: 'What is the status of account 42?',
|
|
470
|
+
})) {
|
|
471
|
+
console.log(event.type, event.payload);
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The stream emits `run.failed` before propagating the original error.
|
|
476
|
+
Cancellation emits `run.cancelled` with a safe `aborted` or `timeout` reason,
|
|
477
|
+
then propagates the original abort reason. `streamAgent` applies backpressure:
|
|
478
|
+
the run advances only while the consumer requests events, and abandoning the
|
|
479
|
+
iterable cancels and persists that local execution.
|
|
480
|
+
|
|
481
|
+
## Sessions and consultable runs
|
|
482
|
+
|
|
483
|
+
Every run belongs to a session. Omit `sessionId` to create one, then pass the
|
|
484
|
+
returned ID to continue the conversation:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
const first = await app.runAgent('support', {
|
|
488
|
+
input: 'What is the status of account 42?',
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const second = await app.runAgent('support', {
|
|
492
|
+
input: 'What should I do next?',
|
|
493
|
+
sessionId: first.sessionId,
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Only completed interactions enter session history. Failed and cancelled runs do
|
|
498
|
+
not affect the next prompt. Replace old history with an application-provided
|
|
499
|
+
summary when needed:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
await app.compactSession(first.sessionId, 'Account 42 is active.');
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Automatic compaction is opt-in through a context budget:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
const support = defineAgent({
|
|
509
|
+
name: 'support',
|
|
510
|
+
instructions: 'Answer clearly.',
|
|
511
|
+
contextBudget: { contextWindow: 128_000, reserveOutputTokens: 8_000 },
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Before each model call, Fevex estimates the prompt. Above 80% of the usable
|
|
516
|
+
window it replaces older completed turns with a summary, keeps system
|
|
517
|
+
instructions, the current request and the latest tool batch intact, and emits
|
|
518
|
+
`context.compacted`. The summary is cached on the session and reused across
|
|
519
|
+
runs; the original history is not rewritten. Use `compaction.compactor` to
|
|
520
|
+
inject a summarizer; the default is a deterministic digest and
|
|
521
|
+
`createModelContextCompactor` provides a model-backed option. Redact event
|
|
522
|
+
summaries with `compaction.redactSummary` and read counters with
|
|
523
|
+
`app.getCompactionStats()`.
|
|
524
|
+
`limits.maxInputTokens` is still the cumulative spend cap, while
|
|
525
|
+
`contextBudget.contextWindow` is the model context window.
|
|
526
|
+
|
|
527
|
+
Use `startAgent` when execution must continue without consuming a stream:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const started = await app.startAgent('support', {
|
|
531
|
+
input: 'Check account 42.',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const run = await app.getRun(started.id);
|
|
535
|
+
const events = await app.listEvents(started.id);
|
|
536
|
+
const laterEvents = await app.listEvents(started.id, {
|
|
537
|
+
after: events.at(-1)?.id,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await app.cancelRun(started.id);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
`AgentRun`, `Session` and events are serializable snapshots. `listEvents` orders
|
|
544
|
+
by `sequence`; `after` accepts an event ID from the same run as a stable cursor.
|
|
545
|
+
Unknown cursors fail instead of silently replaying events.
|
|
546
|
+
|
|
547
|
+
`createFevex` uses `InMemoryRunStore` by default. Supply `runStore` to replace
|
|
548
|
+
it. The default survives only while the process lives, and one run at a time
|
|
549
|
+
may update a session.
|
|
550
|
+
|
|
551
|
+
The built-in durable stores reserve each session atomically when creating an
|
|
552
|
+
agent, workflow or team run. A competing run receives `RUN_CONFLICT` before
|
|
553
|
+
calling its model or tools. A paused or orphaned run keeps its reservation:
|
|
554
|
+
resume, recover or cancel that run before starting another. Lease expiry permits
|
|
555
|
+
recovery of the same run; it does not free its conversation for a new run.
|
|
556
|
+
|
|
557
|
+
`Session.revision` is store-managed. Creation, history commits and compaction
|
|
558
|
+
check the snapshot revision and advance it atomically. Compaction rejects active
|
|
559
|
+
or paused sessions and stale snapshots. On conflict, reload the session and
|
|
560
|
+
decide whether to retry; never automatically retry external effects.
|
|
561
|
+
|
|
562
|
+
Custom durable adapters must implement these guarantees in `createExecution`,
|
|
563
|
+
`commitExecution` and `saveSession`, and pass `testRunStore`.
|
|
564
|
+
`ExecutionCreate.session` is now required for existing sessions as well as new
|
|
565
|
+
ones. A legacy session without a revision is treated as revision zero.
|
|
566
|
+
The low-level `saveRun` API is for trusted storage maintenance; it must not be
|
|
567
|
+
used to bypass runtime reservation or overwrite active runs.
|
|
568
|
+
|
|
569
|
+
Upgrade all workers and adapters together; old workers do not enforce session
|
|
570
|
+
reservation. Every durable commit also validates the lease owner, generation and expiry in
|
|
571
|
+
the same transaction as the run revision. Acquisition advances the generation,
|
|
572
|
+
including when an owner ID is reused. Release expires the lease without deleting
|
|
573
|
+
that counter; stale workers cannot commit, renew or release a newer generation.
|
|
574
|
+
|
|
575
|
+
Custom adapters must accept `ExecutionCommit.lease` and the generation argument
|
|
576
|
+
to `releaseLease`. Initialize `RunLease.generation` to zero for creation or
|
|
577
|
+
acquisition and retain the generation assigned by the store on success.
|
|
578
|
+
Renewal keeps the same generation and cannot revive an expired lease.
|
|
579
|
+
|
|
580
|
+
Renewal errors surface as `FevexRunError` with code `RUN_CONFLICT` and the original
|
|
581
|
+
error in `cause`. Local cleanup does not persist cancellation after ownership is lost.
|
|
582
|
+
A lost, failed or blocked renewal aborts local execution; the runtime will not
|
|
583
|
+
start further work or persist results under that token. The orphaned run remains
|
|
584
|
+
available for recovery. External effects already in flight still need
|
|
585
|
+
idempotency or fencing in the external system.
|
|
586
|
+
|
|
587
|
+
Migrate storage before restarting workers. Legacy leases get generation zero
|
|
588
|
+
and cannot commit or renew; recovery can acquire a new generation after they
|
|
589
|
+
expire. Never run old and new workers together during this upgrade.
|
|
590
|
+
|
|
591
|
+
## HTTP protocol
|
|
592
|
+
|
|
593
|
+
`@fevex/core/http` exposes the runtime through versioned JSON and SSE using
|
|
594
|
+
native Fetch types:
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
import { createFevexHttpHandler, createFevexHttpClient } from '@fevex/core/http';
|
|
598
|
+
|
|
599
|
+
const handler = createFevexHttpHandler({ fevex: app });
|
|
600
|
+
const client = createFevexHttpClient({ baseUrl: 'http://localhost:3001' });
|
|
601
|
+
|
|
602
|
+
const run = await client.startRun('support', { input: 'Check account 42.' });
|
|
603
|
+
for await (const event of client.observeRun(run.id)) {
|
|
604
|
+
console.log(event.type);
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Protocol v3 also exposes `client.startWorkflow(...)` and
|
|
609
|
+
`client.startTeam(...)`; all run kinds share observation, resume, recovery and
|
|
610
|
+
cancellation endpoints.
|
|
611
|
+
|
|
612
|
+
Hosts pass authenticated `ExecutionContext` separately to `handler(request,
|
|
613
|
+
{ context })`. Observation reconnects with `Last-Event-ID`; disconnecting an
|
|
614
|
+
SSE response never cancels its run. Cancellation is explicit through
|
|
615
|
+
`client.cancelRun(run.id)`.
|
|
616
|
+
|
|
617
|
+
Artifacts use the same handler:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
const handler = createFevexHttpHandler({
|
|
621
|
+
fevex: app,
|
|
622
|
+
artifacts: artifactStore, // FileSystemArtifactStore or S3ArtifactStore
|
|
623
|
+
artifactCatalog: artifactCatalog, // optional; enables listing by run
|
|
624
|
+
authorizeArtifact(access) {
|
|
625
|
+
return access.context?.actor?.id === 'alice';
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Routes:
|
|
631
|
+
|
|
632
|
+
- `GET /v1/artifacts/:id` streams the bytes with `content-type`,
|
|
633
|
+
`content-length`, `content-disposition`, `etag` and artifact headers.
|
|
634
|
+
- `HEAD /v1/artifacts/:id` returns the same metadata without a body.
|
|
635
|
+
- `DELETE /v1/artifacts/:id` deletes bytes and catalog metadata.
|
|
636
|
+
- `GET /v1/runs/:runId/artifacts?limit=100` lists descriptors for a run.
|
|
637
|
+
|
|
638
|
+
The client exposes `getArtifact`, `listArtifacts` and `deleteArtifact`. Keep the
|
|
639
|
+
bucket private; the handler authorizes the request and streams the bytes, or a
|
|
640
|
+
host can use the descriptor to mint a short-lived signed URL.
|
|
641
|
+
|
|
642
|
+
## Approvals and durable runs
|
|
643
|
+
|
|
644
|
+
Tools can declare risk, approval, idempotency, retries and credential names.
|
|
645
|
+
Approval pauses before credentials are resolved or the effect starts:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
const charge = defineTool({
|
|
649
|
+
name: 'charge',
|
|
650
|
+
risk: 'sensitive',
|
|
651
|
+
approval: 'required',
|
|
652
|
+
idempotency: 'keyed',
|
|
653
|
+
credentials: ['payments-api-key'],
|
|
654
|
+
retry: { maxAttempts: 3, backoffMs: 200, maxBackoffMs: 2_000 },
|
|
655
|
+
async execute(input, context) {
|
|
656
|
+
const apiKey = await context.getCredential('payments-api-key');
|
|
657
|
+
return chargeCustomer(input, {
|
|
658
|
+
apiKey,
|
|
659
|
+
idempotencyKey: context.idempotencyKey,
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
`runAgent` throws `RunPausedError`, `streamAgent` emits `run.paused`, and
|
|
666
|
+
`startAgent` leaves the paused snapshot consultable. Continue an approval from
|
|
667
|
+
any runtime sharing the durable store:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
await app.resumeRun(runId, {
|
|
671
|
+
type: 'approval',
|
|
672
|
+
approvalId,
|
|
673
|
+
decision: 'approve',
|
|
674
|
+
actor: { id: 'reviewer-42' },
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Use `InMemoryRunStore` for ephemeral runs, `@fevex/sqlite` for local durability,
|
|
679
|
+
or `@fevex/postgres` across multiple processes:
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
import { createSQLiteRunStore } from '@fevex/sqlite';
|
|
683
|
+
|
|
684
|
+
const runStore = createSQLiteRunStore({
|
|
685
|
+
filename: '.fevex/runs.db',
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
SQLite creates and migrates its local file automatically. PostgreSQL migrations
|
|
690
|
+
remain explicit and are intended for server deployments.
|
|
691
|
+
|
|
692
|
+
Policies run before approval resolution, credentials and each effect or retry.
|
|
693
|
+
Credential values are not stored or sent to the model, and tool outputs that
|
|
694
|
+
contain a resolved value are rejected.
|
|
695
|
+
|
|
696
|
+
## Limits
|
|
697
|
+
|
|
698
|
+
Agent runs default to 8 model steps and 16 sequential tool calls. Override
|
|
699
|
+
them on the agent:
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
defineAgent({
|
|
703
|
+
name: 'support',
|
|
704
|
+
instructions: 'Answer account questions clearly.',
|
|
705
|
+
limits: {
|
|
706
|
+
maxSteps: 4,
|
|
707
|
+
maxToolCalls: 6,
|
|
708
|
+
maxInputTokens: 10_000,
|
|
709
|
+
maxOutputTokens: 2_000,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Input token limits are cumulative run budgets checked from provider usage after
|
|
715
|
+
each model response. Output token limits are also sent to model gateways as the
|
|
716
|
+
remaining preventive cap, then verified from reported usage. A configured token
|
|
717
|
+
limit fails explicitly when the gateway does not report the corresponding usage
|
|
718
|
+
field. Set a token limit to `false` to leave it unlimited.
|
|
719
|
+
|
|
720
|
+
Cancellation is immediate from the caller's perspective. Models, tools and
|
|
721
|
+
schema validators still need to honor the supplied `AbortSignal` to stop their
|
|
722
|
+
underlying work. External systems must enforce the supplied idempotency key to
|
|
723
|
+
make keyed retries safe. Repeated tool call IDs are rejected within a run before
|
|
724
|
+
Fevex repeats an effect.
|
|
725
|
+
|
|
726
|
+
### Deadlines, admission and retention
|
|
727
|
+
|
|
728
|
+
Set `deadlineMs` on a run request to cap wall-clock time. An expired deadline
|
|
729
|
+
cancels the run with `run.cancelled` reason `timeout`; resuming or recovering
|
|
730
|
+
after the absolute deadline persists that cancellation instead of continuing.
|
|
731
|
+
|
|
732
|
+
`createFevex({ admission })` limits concurrent top-level runs globally or per
|
|
733
|
+
tenant. The default tenant key is a non-empty `context.attributes.tenantId`;
|
|
734
|
+
`tenantKey` and `authorize` support host-specific quotas. Counts are local to
|
|
735
|
+
one runtime process.
|
|
736
|
+
|
|
737
|
+
`createFevex({ limits: { maxEventsPerRun } })` caps non-control events per run.
|
|
738
|
+
Pause, resume, approval, elicitation, wait and terminal events are always
|
|
739
|
+
persisted so a run can always reach a durable state; use retention for a total
|
|
740
|
+
storage cap.
|
|
741
|
+
|
|
742
|
+
Terminal runs can be purged with `app.purgeRuns({ before, statuses, limit })`.
|
|
743
|
+
SQLite and PostgreSQL implement the retention capability; custom stores can add
|
|
744
|
+
`purgeRuns` to opt in. The API deletes only terminal runs and sessions left
|
|
745
|
+
without runs; active work is untouched. Artifact retention and per-event payload
|
|
746
|
+
byte limits remain host policies.
|
|
747
|
+
|
|
748
|
+
## Observability
|
|
749
|
+
|
|
750
|
+
Add trace exporters to the application configuration; agent definitions stay
|
|
751
|
+
unchanged:
|
|
752
|
+
|
|
753
|
+
```ts
|
|
754
|
+
import { createOpenTelemetryExporter } from '@fevex/opentelemetry';
|
|
755
|
+
|
|
756
|
+
const app = createFevex({
|
|
757
|
+
models,
|
|
758
|
+
agents,
|
|
759
|
+
tools,
|
|
760
|
+
observability: {
|
|
761
|
+
exporters: [createOpenTelemetryExporter()],
|
|
762
|
+
calculateCost({ usage }) {
|
|
763
|
+
return {
|
|
764
|
+
amount: (usage.totalTokens ?? 0) * 0.000_001,
|
|
765
|
+
currency: 'USD',
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
await app.runAgent('support', { input: 'Hello' });
|
|
772
|
+
await app.flushObservability();
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Traces contain only metadata by default. Output and safe error capture require
|
|
776
|
+
an explicit `content.include` policy; sensitive object keys are redacted before
|
|
777
|
+
an optional application redactor runs. Exporters run outside the run path: their
|
|
778
|
+
errors never change the run result, and `flushObservability()` reports pending
|
|
779
|
+
export failures. Export work is bounded and deduplicated:
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
const app = createFevex({
|
|
783
|
+
models,
|
|
784
|
+
agents,
|
|
785
|
+
observability: {
|
|
786
|
+
exporters: [createOpenTelemetryExporter()],
|
|
787
|
+
exportQueue: {
|
|
788
|
+
concurrency: 4,
|
|
789
|
+
maxQueued: 100,
|
|
790
|
+
timeoutMs: 30_000,
|
|
791
|
+
maxFailures: 100,
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
await app.flushObservability({ timeoutMs: 5_000 });
|
|
797
|
+
console.log(app.getObservabilityStats());
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Workflow and team traces carry `kind` and a span per step. Agent steps expose
|
|
801
|
+
`childRunId`; child agent traces carry `parentRunId` and `rootRunId`, and tool
|
|
802
|
+
spans are nested under the model call that requested them. A dropped trace or a
|
|
803
|
+
failed export is counted in `getObservabilityStats()`, and the next
|
|
804
|
+
`flushObservability()` rejects with the failures accumulated since the previous
|
|
805
|
+
flush. OpenTelemetry SDK setup, OTLP export and shutdown remain application
|
|
806
|
+
responsibilities.
|
|
807
|
+
|
|
808
|
+
## Graceful shutdown
|
|
809
|
+
|
|
810
|
+
`shutdown` stops admission, drains active runs, cancels what remains at the
|
|
811
|
+
deadline and flushes trace exports:
|
|
812
|
+
|
|
813
|
+
```ts
|
|
814
|
+
const result = await app.shutdown({ timeoutMs: 30_000 });
|
|
815
|
+
console.log(result.timedOut, result.cancelledRunIds, result.observability);
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
New starts, resumes and recoveries fail with `RUNTIME_CLOSED` (HTTP `503`) once
|
|
819
|
+
shutdown begins. The host closes its HTTP server, stores and external adapters
|
|
820
|
+
after `shutdown` returns. See
|
|
821
|
+
[Operations & soak tests](https://fevex.dev/docs/operations/) for crash recovery
|
|
822
|
+
and the 1–8 hour load harness.
|
|
823
|
+
|
|
824
|
+
## Evaluations
|
|
825
|
+
|
|
826
|
+
`@fevex/core/evals` runs local datasets sequentially with fresh sessions:
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
import {
|
|
830
|
+
exactOutputScorer,
|
|
831
|
+
runEvaluation,
|
|
832
|
+
toolSelectionScorer,
|
|
833
|
+
} from '@fevex/core/evals';
|
|
834
|
+
|
|
835
|
+
const report = await runEvaluation({
|
|
836
|
+
app,
|
|
837
|
+
suiteName: 'support-regression',
|
|
838
|
+
targetVersion: 'v2',
|
|
839
|
+
agentName: 'support',
|
|
840
|
+
dataset: {
|
|
841
|
+
name: 'support',
|
|
842
|
+
version: '1',
|
|
843
|
+
cases: [{
|
|
844
|
+
id: 'active-account',
|
|
845
|
+
input: 'Check account 42',
|
|
846
|
+
expected: {
|
|
847
|
+
output: { answer: 'Account 42 is active.' },
|
|
848
|
+
tools: ['accounts_get'],
|
|
849
|
+
},
|
|
850
|
+
}],
|
|
851
|
+
},
|
|
852
|
+
scorers: [exactOutputScorer(), toolSelectionScorer()],
|
|
853
|
+
});
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Built-in deterministic scorers cover exact output, ordered or unordered tool
|
|
857
|
+
selection, forbidden tools, max latency, max tokens and max cost. Use
|
|
858
|
+
`serializeEvalReport` for a canonical, versionable JSON baseline, `jsonReporter`
|
|
859
|
+
or `textReporter` with your own write callback, and `compareEvaluationReports`
|
|
860
|
+
to fail CI when cases disappear, passing scores fail or scores drop beyond the
|
|
861
|
+
configured tolerance.
|
|
862
|
+
|
|
863
|
+
## Connections
|
|
864
|
+
|
|
865
|
+
Connections turn remote tool providers into normal Fevex tools. Names are local
|
|
866
|
+
and explicit: a connection named `docs` with remote tool `search` is exposed to
|
|
867
|
+
agents as `docs__search`.
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
import { createFevex, defineAgent, defineConnection } from '@fevex/core';
|
|
871
|
+
import { createMcpToolProvider } from '@fevex/mcp';
|
|
872
|
+
|
|
873
|
+
const docs = defineConnection({
|
|
874
|
+
name: 'docs',
|
|
875
|
+
provider: createMcpToolProvider({ url: 'https://example.com/mcp' }),
|
|
876
|
+
allowlist: ['search'],
|
|
877
|
+
timeoutMs: 30_000,
|
|
878
|
+
tools: {
|
|
879
|
+
search: {
|
|
880
|
+
description: 'Search docs.',
|
|
881
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
createFevex({
|
|
887
|
+
models: {},
|
|
888
|
+
agents: [defineAgent({
|
|
889
|
+
name: 'assistant',
|
|
890
|
+
instructions: 'Help.',
|
|
891
|
+
tools: ['docs__search'],
|
|
892
|
+
})],
|
|
893
|
+
connections: [docs],
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Fevex owns allowlists, timeouts, approvals, policies and safe error messages.
|
|
898
|
+
MCP metadata and annotations are treated as untrusted hints. The first MCP
|
|
899
|
+
adapter supports Streamable HTTP only; `stdio`, resources, prompts and legacy
|
|
900
|
+
HTTP+SSE stay out of scope.
|
|
901
|
+
|
|
902
|
+
OpenAPI providers use the same connection contract:
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
import { createOpenApiToolProvider } from '@fevex/openapi';
|
|
906
|
+
|
|
907
|
+
const billing = defineConnection({
|
|
908
|
+
name: 'billing',
|
|
909
|
+
provider: createOpenApiToolProvider({
|
|
910
|
+
document: openApiDocument,
|
|
911
|
+
operations: { allow: ['getInvoice'] },
|
|
912
|
+
headers: () => ({ Authorization: `Bearer ${process.env.BILLING_TOKEN}` }),
|
|
913
|
+
}),
|
|
914
|
+
allowlist: ['getInvoice'],
|
|
915
|
+
});
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
`@fevex/openapi` accepts bundled JSON OpenAPI 3.1.x documents, local refs and
|
|
919
|
+
JSON request/response bodies. It rejects unsupported JSON Schema keywords,
|
|
920
|
+
remote refs, YAML, OpenAPI 3.0/3.2 and non-JSON payloads instead of silently
|
|
921
|
+
dropping constraints.
|
|
922
|
+
|
|
923
|
+
## Tool search and large results
|
|
924
|
+
|
|
925
|
+
Agents with large tool catalogs can expose `fevex__search_tools` instead of
|
|
926
|
+
sending every schema on every turn:
|
|
927
|
+
|
|
928
|
+
```ts
|
|
929
|
+
defineAgent({
|
|
930
|
+
name: 'support',
|
|
931
|
+
instructions: 'Answer with the available tools.',
|
|
932
|
+
tools: [...],
|
|
933
|
+
toolSearch: { mode: 'auto', threshold: 30, pinned: ['accounts_get'] },
|
|
934
|
+
});
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
`auto` enables search above the threshold, `on` always does and `off` exposes
|
|
938
|
+
every tool. Search only indexes the agent's own tools; allowlists, policies and
|
|
939
|
+
approvals still apply when a loaded tool executes. Add `semantic.embeddings` to
|
|
940
|
+
fuse lexical and vector candidates with Reciprocal Rank Fusion; failures fall
|
|
941
|
+
back to lexical. An optional `semantic.reranker` can reorder the fused top
|
|
942
|
+
candidates.
|
|
943
|
+
|
|
944
|
+
Tools can cap serialized results with `maxResultBytes`, or an agent/run can set
|
|
945
|
+
`limits.maxToolResultBytes`. The default cap is 1 MiB; use `false` to disable it.
|
|
946
|
+
Store larger outputs through `context.artifacts` and return the descriptor;
|
|
947
|
+
otherwise the run fails with `TOOL_RESULT_TOO_LARGE`.
|
|
948
|
+
|
|
949
|
+
## Channels
|
|
950
|
+
|
|
951
|
+
`@fevex/core/channels` provides a small adapter contract and `handleChannelInput`
|
|
952
|
+
for hosting apps that receive messages from another surface:
|
|
953
|
+
|
|
954
|
+
```ts
|
|
955
|
+
import { handleChannelInput } from '@fevex/core/channels';
|
|
956
|
+
|
|
957
|
+
const sessions = new Map<string, string>();
|
|
958
|
+
|
|
959
|
+
const result = await handleChannelInput(
|
|
960
|
+
{ id: 'm1', conversationId: 'c1', text: 'hello' },
|
|
961
|
+
{
|
|
962
|
+
fevex: app,
|
|
963
|
+
agentName: 'assistant',
|
|
964
|
+
adapter: {
|
|
965
|
+
name: 'memory',
|
|
966
|
+
async parse(input) {
|
|
967
|
+
return {
|
|
968
|
+
id: input.id,
|
|
969
|
+
deliveryId: input.id,
|
|
970
|
+
conversationId: input.conversationId,
|
|
971
|
+
content: input.text,
|
|
972
|
+
};
|
|
973
|
+
},
|
|
974
|
+
async deliver(output) {
|
|
975
|
+
return output;
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
resolveSessionId(message) {
|
|
979
|
+
return sessions.get(message.conversationId);
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
if (!result.ignored) sessions.set(result.message.conversationId, result.run.sessionId);
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
The helper emits local channel events around the run. Slack verification,
|
|
988
|
+
acknowledgement, durable redelivery and deduplication remain outside this core
|
|
989
|
+
helper and belong to the Slack adapter phase.
|
|
990
|
+
|
|
991
|
+
## Knowledge and memory
|
|
992
|
+
|
|
993
|
+
`@fevex/core/knowledge` keeps external context separate from sessions:
|
|
994
|
+
|
|
995
|
+
```ts
|
|
996
|
+
import {
|
|
997
|
+
InMemoryMemoryStore,
|
|
998
|
+
createFevex,
|
|
999
|
+
defineAgent,
|
|
1000
|
+
defineContextProvider,
|
|
1001
|
+
defineSkill,
|
|
1002
|
+
} from '@fevex/core';
|
|
1003
|
+
|
|
1004
|
+
const app = createFevex({
|
|
1005
|
+
models,
|
|
1006
|
+
memoryStore: new InMemoryMemoryStore(),
|
|
1007
|
+
contextProviders: [
|
|
1008
|
+
defineSkill({
|
|
1009
|
+
name: 'refunds',
|
|
1010
|
+
instructions: 'Refunds are allowed for 30 days after purchase.',
|
|
1011
|
+
}),
|
|
1012
|
+
defineContextProvider({
|
|
1013
|
+
name: 'account',
|
|
1014
|
+
async read({ context }) {
|
|
1015
|
+
return [{ id: 'tier', content: `Plan: ${context?.attributes?.plan ?? 'free'}` }];
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
],
|
|
1019
|
+
agents: [
|
|
1020
|
+
defineAgent({
|
|
1021
|
+
name: 'support',
|
|
1022
|
+
instructions: 'Answer with business context.',
|
|
1023
|
+
skills: ['refunds'],
|
|
1024
|
+
context: ['account'],
|
|
1025
|
+
memory: { read: true, write: true },
|
|
1026
|
+
}),
|
|
1027
|
+
],
|
|
1028
|
+
});
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
For longer instructions, keep them in an explicit `SKILL.md` file. Only the
|
|
1032
|
+
frontmatter metadata enters the prompt; the body loads on demand:
|
|
1033
|
+
|
|
1034
|
+
```ts
|
|
1035
|
+
import { createAgentsMdContextProvider, loadSkillFile } from '@fevex/core';
|
|
1036
|
+
|
|
1037
|
+
const refunds = await loadSkillFile('./skills/refunds/SKILL.md');
|
|
1038
|
+
|
|
1039
|
+
const app = createFevex({
|
|
1040
|
+
models,
|
|
1041
|
+
contextProviders: [refunds, createAgentsMdContextProvider('./AGENTS.md')],
|
|
1042
|
+
agents: [
|
|
1043
|
+
defineAgent({
|
|
1044
|
+
name: 'support',
|
|
1045
|
+
instructions: 'Answer with business context.',
|
|
1046
|
+
skills: ['refunds'],
|
|
1047
|
+
context: ['agents-md'],
|
|
1048
|
+
}),
|
|
1049
|
+
],
|
|
1050
|
+
});
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
`SKILL.md` uses `name` and `description` frontmatter. The reserved
|
|
1054
|
+
`fevex__load_skill` tool loads the body and lists files under `references/`,
|
|
1055
|
+
`scripts/` and `assets/`; scripts are not executed by the runtime. Paths are
|
|
1056
|
+
explicit, with no automatic discovery.
|
|
1057
|
+
|
|
1058
|
+
Context providers are lazy per run. Memory is a replaceable store; the built-in
|
|
1059
|
+
store is in-memory substring search, not embeddings, vector search or durable
|
|
1060
|
+
storage.
|
|
1061
|
+
|
|
1062
|
+
## Artifacts
|
|
1063
|
+
|
|
1064
|
+
`@fevex/core/artifacts` stores large or binary outputs as references. Runs,
|
|
1065
|
+
events and prompts carry an `ArtifactDescriptor`; bytes stay in an
|
|
1066
|
+
`ArtifactStore`.
|
|
1067
|
+
|
|
1068
|
+
```ts
|
|
1069
|
+
import {
|
|
1070
|
+
FileSystemArtifactStore,
|
|
1071
|
+
InMemoryArtifactStore,
|
|
1072
|
+
S3ArtifactStore,
|
|
1073
|
+
type S3ObjectClient,
|
|
1074
|
+
} from '@fevex/core/artifacts';
|
|
1075
|
+
|
|
1076
|
+
const artifacts = new FileSystemArtifactStore({
|
|
1077
|
+
rootDir: '.fevex/artifacts',
|
|
1078
|
+
maxBytes: 10 * 1024 * 1024,
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
const descriptor = await artifacts.put(
|
|
1082
|
+
{
|
|
1083
|
+
filename: 'report.pdf',
|
|
1084
|
+
mimeType: 'application/pdf',
|
|
1085
|
+
bytes,
|
|
1086
|
+
},
|
|
1087
|
+
{ tenantId: 'acme', runId: 'run-1' },
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
const stored = await artifacts.get(descriptor.artifactId, { tenantId: 'acme' });
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Use `InMemoryArtifactStore` for tests. For S3, R2, MinIO or another
|
|
1094
|
+
S3-compatible service, wrap its SDK with the small `S3ObjectClient` interface;
|
|
1095
|
+
Fevex does not ship an object-storage SDK:
|
|
1096
|
+
|
|
1097
|
+
```ts
|
|
1098
|
+
const client: S3ObjectClient = {
|
|
1099
|
+
async putObject({ key, body, contentType }) {
|
|
1100
|
+
await s3.send(new PutObjectCommand({
|
|
1101
|
+
Bucket,
|
|
1102
|
+
Key: key,
|
|
1103
|
+
Body: body,
|
|
1104
|
+
ContentType: contentType,
|
|
1105
|
+
}));
|
|
1106
|
+
},
|
|
1107
|
+
async getObject({ key }) {
|
|
1108
|
+
try {
|
|
1109
|
+
const result = await s3.send(new GetObjectCommand({ Bucket, Key: key }));
|
|
1110
|
+
return new Uint8Array(await result.Body!.transformToByteArray());
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if ((error as { name?: string }).name === 'NoSuchKey') return null;
|
|
1113
|
+
throw error;
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
async deleteObject({ key }) {
|
|
1117
|
+
await s3.send(new DeleteObjectCommand({ Bucket, Key: key }));
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const artifacts = new S3ArtifactStore({ client, prefix: 'fevex' });
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
The bucket should stay private. The host authorizes the actor and serves a
|
|
1125
|
+
short-lived signed URL or streams the bytes; do not expose bucket URLs to the
|
|
1126
|
+
model. This subpath is local-first: use the filesystem store for development and
|
|
1127
|
+
single-node deployments, and the S3-compatible store for multi-instance
|
|
1128
|
+
production.
|
|
1129
|
+
|
|
1130
|
+
For a queryable metadata index, compose the byte store with a catalog:
|
|
1131
|
+
|
|
1132
|
+
```ts
|
|
1133
|
+
import { CatalogedArtifactStore, FileSystemArtifactStore } from '@fevex/core/artifacts';
|
|
1134
|
+
import { createSQLiteArtifactCatalog } from '@fevex/sqlite';
|
|
1135
|
+
|
|
1136
|
+
const catalog = createSQLiteArtifactCatalog({ filename: '.fevex/artifacts.db' });
|
|
1137
|
+
const artifacts = new CatalogedArtifactStore({
|
|
1138
|
+
store: new FileSystemArtifactStore({ rootDir: '.fevex/artifacts' }),
|
|
1139
|
+
catalog,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
const records = await artifacts.list({ runId: 'run-1' }, { tenantId: 'acme' });
|
|
1143
|
+
await artifacts.deleteByRun('run-1', { tenantId: 'acme' });
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
`@fevex/postgres` exposes the same catalog contract with
|
|
1147
|
+
`createPostgresArtifactCatalog`. The catalog stores only metadata; bytes stay in
|
|
1148
|
+
the filesystem or object storage.
|
|
1149
|
+
|
|
1150
|
+
Pass the store to `createFevex` to expose it to tools as `context.artifacts`:
|
|
1151
|
+
|
|
1152
|
+
```ts
|
|
1153
|
+
const save = defineTool({
|
|
1154
|
+
name: 'save_report',
|
|
1155
|
+
async execute(_input, context) {
|
|
1156
|
+
return context.artifacts!.put({
|
|
1157
|
+
filename: 'report.txt',
|
|
1158
|
+
mimeType: 'text/plain',
|
|
1159
|
+
bytes: new TextEncoder().encode('report'),
|
|
1160
|
+
});
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
createFevex({
|
|
1165
|
+
models,
|
|
1166
|
+
agents,
|
|
1167
|
+
tools: [save],
|
|
1168
|
+
artifacts,
|
|
1169
|
+
});
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
The runtime injects `runId`, `sessionId`, tenant and actor into every artifact
|
|
1173
|
+
call. Each successful `put` emits `artifact.created` with the descriptor before
|
|
1174
|
+
`tool.completed`; a tool that writes and then fails still leaves the event and
|
|
1175
|
+
the artifact available.
|
|
215
1176
|
|
|
216
1177
|
## Public Exports
|
|
217
1178
|
|
|
218
1179
|
| Import | Purpose |
|
|
219
1180
|
| --- | --- |
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
|
|
|
1181
|
+
| `@fevex/core` | Common runtime, definitions and contracts |
|
|
1182
|
+
| `@fevex/core/agents` | Agent definitions |
|
|
1183
|
+
| `@fevex/core/artifacts` | Artifact descriptors, local stores and S3-compatible adapter |
|
|
1184
|
+
| `@fevex/core/channels` | Channel adapters and message handling |
|
|
1185
|
+
| `@fevex/core/knowledge` | Context providers, skills and memory contracts |
|
|
1186
|
+
| `@fevex/core/models` | Provider-neutral model contracts |
|
|
1187
|
+
| `@fevex/core/tools` | Tool, connection and provider contracts |
|
|
1188
|
+
| `@fevex/core/workflows` | Durable workflow definitions |
|
|
1189
|
+
| `@fevex/core/teams` | Multiagent team definitions |
|
|
1190
|
+
| `@fevex/core/runtime` | Runs, sessions and store contracts |
|
|
1191
|
+
| `@fevex/core/sandbox` | Sandbox contract and local development sandbox |
|
|
1192
|
+
| `@fevex/core/policies` | Authorization policy contracts |
|
|
1193
|
+
| `@fevex/core/observability` | Trace, bounded export queue, redaction and cost contracts |
|
|
1194
|
+
| `@fevex/core/evals` | Datasets, scorers, reporters and regressions |
|
|
1195
|
+
| `@fevex/core/testing` | Deterministic testing helpers |
|
|
1196
|
+
|
|
1197
|
+
All subpaths ship in the same `@fevex/core` package.
|
|
1198
|
+
|
|
1199
|
+
## Source Layout
|
|
226
1200
|
|
|
227
|
-
|
|
1201
|
+
Each public subpath is a folder under `src`. Its `index.ts` is intentionally a
|
|
1202
|
+
small export barrel; contracts and implementations live in files named after
|
|
1203
|
+
their responsibility. For example, `runtime/run-store.ts` owns persistence
|
|
1204
|
+
contracts while `runtime/in-memory-run-store.ts` owns the development
|
|
1205
|
+
implementation. Keep provider-neutral public contracts in their domain folder
|
|
1206
|
+
and orchestration details under `src/internal`.
|
|
1207
|
+
|
|
1208
|
+
When adding a feature:
|
|
1209
|
+
|
|
1210
|
+
- extend an existing responsibility before creating a new abstraction;
|
|
1211
|
+
- keep platform or provider-specific behavior outside `@fevex/core`;
|
|
1212
|
+
- export public API only through the domain barrel and preserve existing
|
|
1213
|
+
subpath imports;
|
|
1214
|
+
- document lifecycle, persistence, security and replay semantics; avoid
|
|
1215
|
+
comments that only restate the TypeScript signature;
|
|
1216
|
+
- add the smallest test that exercises the behavior at its public boundary.
|
|
228
1217
|
|
|
229
1218
|
## Current Scope
|
|
230
1219
|
|
|
231
1220
|
The MVP intentionally supports:
|
|
232
1221
|
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
- synchronous event observers;
|
|
1222
|
+
- bounded multi-step model-tool loops with sequential tool execution;
|
|
1223
|
+
- native provider streaming and durable, consultable runs;
|
|
1224
|
+
- synchronous event observers and `AsyncIterable` event streaming;
|
|
1225
|
+
- local traces, OpenTelemetry export and deterministic evaluation suites;
|
|
236
1226
|
- optional Standard Schema validation;
|
|
237
|
-
- `AbortSignal` cancellation.
|
|
1227
|
+
- immediate `AbortSignal` cancellation with cooperative underlying work.
|
|
238
1228
|
|
|
239
|
-
It does not
|
|
240
|
-
|
|
241
|
-
runtime effect yet.
|
|
1229
|
+
It does not include a CLI, hosted observability service, model-judge evals or
|
|
1230
|
+
human evaluation workflows.
|
|
242
1231
|
|
|
243
1232
|
## License
|
|
244
1233
|
|
|
245
1234
|
Apache-2.0
|
|
1235
|
+
|
|
1236
|
+
### Connection discovery isolation
|
|
1237
|
+
|
|
1238
|
+
Connection tool metadata is discovered for each resolution using that caller's
|
|
1239
|
+
context. The core does not cache catalogs or rejected discovery promises across
|
|
1240
|
+
calls. A failed discovery can therefore recover on the next attempt without
|
|
1241
|
+
recreating the runtime. Providers must scope their own state to an identity;
|
|
1242
|
+
`@fevex/mcp` binds each instance to its first resolved header set and rejects
|
|
1243
|
+
changes. Use a separate MCP provider/connection per identity and recreate it when
|
|
1244
|
+
credentials rotate. This adds discovery requests in exchange for avoiding stale
|
|
1245
|
+
or cross-identity catalogs. Tool calls are not automatically replayed by MCP.
|
|
1246
|
+
|
|
1247
|
+
### Paged event reads and SSE
|
|
1248
|
+
|
|
1249
|
+
`listEvents(runId, { after, limit, order })` returns events whose sequence is
|
|
1250
|
+
strictly greater than the cursor's sequence. `order` is `asc` by default;
|
|
1251
|
+
`{ order: 'desc', limit: 1 }` retrieves the latest event. `limit` must be a positive
|
|
1252
|
+
safe integer. Omitting it preserves the existing complete-result behavior.
|
|
1253
|
+
Unknown cursors and cursors belonging to another run reject with
|
|
1254
|
+
`FevexRunError` code `INVALID_CURSOR` (HTTP 400).
|
|
1255
|
+
|
|
1256
|
+
SQLite and PostgreSQL resolve the cursor by ID and filter, order and limit in
|
|
1257
|
+
SQL using the existing `(run_id, sequence)` index. Prior event payloads are not
|
|
1258
|
+
loaded to find the cursor. Custom run stores must implement these options and
|
|
1259
|
+
the cursor error contract before being used with the updated HTTP handler.
|
|
1260
|
+
|
|
1261
|
+
The HTTP handler accepts `eventPageSize` (default 100, range 1–1000). It uses this
|
|
1262
|
+
limit for the initial SSE read and every subsequent read. Full pages drain
|
|
1263
|
+
without the polling delay; short pages wait for `pollIntervalMs`. Stream demand
|
|
1264
|
+
controls progress, so a slow consumer retains at most one page plus the stream's
|
|
1265
|
+
single queued frame, rather than buffering the run's entire history. Disconnecting
|
|
1266
|
+
cancels polling. `maxConnections` caps concurrent SSE streams. This bounds event
|
|
1267
|
+
counts, not the byte size of an individual event; per-event payload limits remain
|
|
1268
|
+
host responsibilities. Use `purgeRuns` for configurable retention of terminal
|
|
1269
|
+
runs.
|
|
1270
|
+
|
|
1271
|
+
### Run model identity
|
|
1272
|
+
|
|
1273
|
+
Agent runs retain the effective `modelName` and a `modelMetadata` snapshot when
|
|
1274
|
+
the gateway supplies metadata. Traces and cost calculators use this identity,
|
|
1275
|
+
including per-request model overrides, after terminal checkpoint cleanup and
|
|
1276
|
+
across pause/resume. These fields are optional for older stored runs; resuming
|
|
1277
|
+
an older run fills them from its checkpoint's resolved model.
|
|
1278
|
+
|
|
1279
|
+
### Local sandbox limits
|
|
1280
|
+
|
|
1281
|
+
The local development sandbox sends `SIGTERM` on timeout or cancellation, then
|
|
1282
|
+
`SIGKILL` after 100 ms if the process has not closed. Cancellation rejects with
|
|
1283
|
+
the caller's reason after process cleanup. Output limits apply separately to
|
|
1284
|
+
stdout and stderr in UTF-8 bytes, omitting an incomplete trailing character.
|
|
1285
|
+
This subprocess control does not provide operating-system isolation.
|
|
1286
|
+
|
|
1287
|
+
### Production sandbox
|
|
1288
|
+
|
|
1289
|
+
Use `@fevex/sandbox-docker` for isolated runs. It creates one long-lived
|
|
1290
|
+
container and workspace per run, uses `--network none` by default, drops Linux
|
|
1291
|
+
capabilities, applies PID, memory and CPU limits, and only forwards explicit
|
|
1292
|
+
environment variables. Fevex calls `cleanupRun` after terminal runs and
|
|
1293
|
+
`dispose` during shutdown; paused runs keep their resources for resume.
|
|
1294
|
+
|
|
1295
|
+
```ts
|
|
1296
|
+
import { createDockerSandbox } from '@fevex/sandbox-docker';
|
|
1297
|
+
|
|
1298
|
+
const sandbox = createDockerSandbox({
|
|
1299
|
+
image: 'oven/bun:1-alpine',
|
|
1300
|
+
commands: ['bun', 'sh'],
|
|
1301
|
+
workspaceRoot: '/var/lib/fevex/sandboxes',
|
|
1302
|
+
});
|
|
1303
|
+
```
|