@fevex/core 0.1.0-alpha.0 → 0.1.0-alpha.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.
Files changed (47) hide show
  1. package/README.md +770 -52
  2. package/dist/agents/index.d.mts +1 -22
  3. package/dist/agents/index.mjs +1 -1
  4. package/dist/channels/index.d.mts +2 -0
  5. package/dist/channels/index.mjs +150 -0
  6. package/dist/evals/index.d.mts +153 -0
  7. package/dist/evals/index.mjs +422 -0
  8. package/dist/fevex-Ddyi6z6L.d.mts +60 -0
  9. package/dist/http/index.d.mts +90 -0
  10. package/dist/http/index.mjs +470 -0
  11. package/dist/index-B4PIkqOU.d.mts +80 -0
  12. package/dist/index-Bg9rr8Qe.d.mts +50 -0
  13. package/dist/index-Bgcxgy66.d.mts +82 -0
  14. package/dist/index-CTIwrVf9.d.mts +51 -0
  15. package/dist/index-CqOjvtQ9.d.mts +132 -0
  16. package/dist/index-CrOId2px.d.mts +66 -0
  17. package/dist/index-DeXUtyd9.d.mts +70 -0
  18. package/dist/index-DviHtsLo.d.mts +58 -0
  19. package/dist/index-Dx_LotWZ.d.mts +425 -0
  20. package/dist/index-dkFyTNh4.d.mts +21 -0
  21. package/dist/index.d.mts +39 -15
  22. package/dist/index.mjs +3505 -194
  23. package/dist/knowledge/index.d.mts +2 -0
  24. package/dist/knowledge/index.mjs +89 -0
  25. package/dist/model-stream-u2xMsEaf.mjs +251 -0
  26. package/dist/models/index.d.mts +2 -27
  27. package/dist/observability/index.d.mts +2 -0
  28. package/dist/observability/index.mjs +1 -0
  29. package/dist/policies/index.d.mts +2 -0
  30. package/dist/policies/index.mjs +6 -0
  31. package/dist/run-error-Cr17wPGH.mjs +23 -0
  32. package/dist/runtime/index.d.mts +2 -17
  33. package/dist/runtime/index.mjs +141 -1
  34. package/dist/sandbox/index.d.mts +2 -0
  35. package/dist/sandbox/index.mjs +122 -0
  36. package/dist/teams/index.d.mts +2 -0
  37. package/dist/teams/index.mjs +2 -0
  38. package/dist/teams-Ctz676G4.mjs +82 -0
  39. package/dist/testing/index.d.mts +34 -8
  40. package/dist/testing/index.mjs +572 -23
  41. package/dist/tools/index.d.mts +3 -31
  42. package/dist/tools/index.mjs +222 -2
  43. package/dist/types-CuoRx12Q.d.mts +333 -0
  44. package/dist/workflows/index.d.mts +2 -0
  45. package/dist/workflows/index.mjs +6 -0
  46. package/package.json +39 -3
  47. 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, single-tool agent runs, but
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,21 +17,27 @@ 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
- - **Testable:** `fevex/testing` includes a deterministic model and shared model
22
- contract checks.
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
  For official model adapters:
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
36
+ npm install @fevex/core @fevex/mcp
37
+ npm install @fevex/core @fevex/openapi
38
+ npm install @fevex/core @fevex/sqlite
39
+ npm install @fevex/core @fevex/postgres
40
+ npm install @fevex/core @fevex/opentelemetry @opentelemetry/api
35
41
  ```
36
42
 
37
43
  Schemas are optional. The examples use Zod 4, but any Standard
@@ -46,8 +52,8 @@ npm install zod
46
52
  This example executes a complete model-tool-model cycle without network access:
47
53
 
48
54
  ```ts
49
- import { createFevex, defineAgent, defineTool } from 'fevex';
50
- import { fakeModel } from 'fevex/testing';
55
+ import { createFevex, defineAgent, defineTool } from '@fevex/core';
56
+ import { fakeModel } from '@fevex/core/testing';
51
57
  import { z } from 'zod';
52
58
 
53
59
  const accountInput = z.object({
@@ -60,7 +66,7 @@ const accountOutput = z.object({
60
66
  });
61
67
 
62
68
  const getAccount = defineTool({
63
- name: 'accounts.get',
69
+ name: 'accounts_get',
64
70
  description: 'Get an account by ID.',
65
71
  inputSchema: accountInput,
66
72
  outputSchema: accountOutput,
@@ -73,11 +79,11 @@ const model = fakeModel(
73
79
  {
74
80
  toolCalls: [{
75
81
  id: 'call-1',
76
- name: 'accounts.get',
82
+ name: 'accounts_get',
77
83
  input: { accountId: 42 },
78
84
  }],
79
85
  },
80
- { output: 'Account 42 is active.' },
86
+ { output: { answer: 'Account 42 is active.' } },
81
87
  );
82
88
 
83
89
  const app = createFevex({
@@ -85,48 +91,188 @@ const app = createFevex({
85
91
  agents: [defineAgent({
86
92
  name: 'support',
87
93
  instructions: 'Answer account questions clearly.',
88
- tools: ['accounts.get'],
89
- outputSchema: z.string(),
94
+ tools: ['accounts_get'],
95
+ outputSchema: z.object({ answer: z.string() }),
90
96
  })],
91
97
  tools: [getAccount],
92
98
  });
93
99
 
94
- const result = await app.runAgent<string, string>('support', {
100
+ const result = await app.runAgent<string, { answer: string }>('support', {
95
101
  input: 'What is the status of account 42?',
96
102
  });
97
103
 
98
- console.log(result.output);
104
+ console.log(result.output.answer);
99
105
  // Account 42 is active.
100
106
  ```
101
107
 
102
108
  `fakeModel` returns its configured responses in order and records every input in
103
109
  `model.calls`.
104
110
 
111
+ ## Durable workflows
112
+
113
+ Workflows require a `DurableRunStore`. Their definition owns the input, output,
114
+ event and limit contracts:
115
+
116
+ ```ts
117
+ const review = defineWorkflow({
118
+ name: 'review',
119
+ version: '2',
120
+ inputSchema: z.object({ draft: z.string() }),
121
+ outputSchema: z.string(),
122
+ events: {
123
+ 'review.approved': {
124
+ payloadSchema: z.object({ approved: z.literal(true) }),
125
+ requireActor: true,
126
+ },
127
+ },
128
+ limits: { maxSteps: 8, maxToolCalls: 12 },
129
+ async run(step, input) {
130
+ const draft = await step.agent('draft', 'writer', { input });
131
+ const approval = await step.waitForEvent('approval', 'review.approved');
132
+ return `${draft.output} (${approval.actor?.id}, ${approval.receivedAt})`;
133
+ },
134
+ });
135
+ ```
136
+
137
+ Input is validated and transformed before the initial checkpoint is stored.
138
+ Output is always validated with the definition schema, including after a pause
139
+ or recovery. Effective workflow limits combine definition and request limits;
140
+ agent steps additionally combine their own agent/request limits. These values
141
+ are checkpointed so recovery cannot silently widen a budget.
142
+
143
+ `stepId` is the durable identity of a step: keep it stable while its meaning is
144
+ stable. Increment `version` when replay order, business rules or step meanings
145
+ change. Completed agent steps are reused on replay and `step.agent` always
146
+ returns `{ runId, sessionId, output, usage? }`, without a transient `events`
147
+ field.
148
+
149
+ Child execution context inherits the workflow context. Namespace, attributes
150
+ and prompt values are merged; an authenticated parent actor cannot be replaced
151
+ by a child. `parallel` waits for every declared task to settle, records durable
152
+ child successes, reports multiple failures in declaration order and resolves
153
+ multiple pauses one at a time. Arbitrary promises used in `parallel` must be
154
+ pure or replay-safe; side effects belong in durable agent/tool steps.
155
+
156
+ External events must be declared. `waitForEvent` returns
157
+ `{ payload, actor, receivedAt }`; payload and required actor are checked before
158
+ the checkpoint changes. Cancelling a workflow first cancels its running or
159
+ paused children. Compensation runs in reverse completed-step order; if both the
160
+ workflow and compensation fail, the terminal error is a stable
161
+ `AggregateError`.
162
+
163
+ Durable executions start with a checkpoint v2 and lease in the same atomic
164
+ store operation. An external worker may recover an orphaned `running` run after
165
+ its lease expires:
166
+
167
+ ```ts
168
+ await app.recoverRun(runId, { actor: { id: 'recovery-worker', type: 'service' } });
169
+ ```
170
+
171
+ Paused runs use `resumeRun`; terminal runs and older checkpoints are rejected.
172
+ Fevex does not poll globally: deployments need an external orphan detector and
173
+ a scheduler that calls `resumeRun` for elapsed timers. Exactly-once protection
174
+ is limited to keyed/idempotent tools and durable steps.
175
+
176
+ ## Advanced teams
177
+
178
+ Teams are an opt-in coordination layer on the durable workflow engine. They
179
+ reuse registered agents and cannot widen an agent's model, tools, policies,
180
+ sandbox or limits:
181
+
182
+ ```ts
183
+ const model = fakeModel(
184
+ { output: 'plan' },
185
+ { output: 'research' },
186
+ { output: 'code' },
187
+ { output: 'approved' },
188
+ );
189
+
190
+ const softwareTeam = defineTeam({
191
+ name: 'software-team',
192
+ version: '1',
193
+ supervisor: 'planner',
194
+ members: [
195
+ { agent: 'researcher', role: 'research' },
196
+ { agent: 'coder', role: 'implementation' },
197
+ { agent: 'reviewer', role: 'review' },
198
+ ],
199
+ limits: { maxDelegations: 12, maxParallel: 2 },
200
+ async run(team, input) {
201
+ const plan = await team.delegate('plan', {
202
+ agent: 'planner',
203
+ task: input,
204
+ expectedOutput: 'Implementation plan',
205
+ });
206
+ const work = await team.parallel('work', {
207
+ research: () =>
208
+ team.delegate('research', { agent: 'researcher', task: plan.output }),
209
+ implementation: () =>
210
+ team.delegate('implementation', { agent: 'coder', task: plan.output }),
211
+ });
212
+ return (
213
+ await team.handoff('review', {
214
+ from: 'coder',
215
+ to: 'reviewer',
216
+ reason: 'Final review',
217
+ task: work,
218
+ })
219
+ ).output;
220
+ },
221
+ });
222
+
223
+ const app = createFevex({ models, agents, teams: [softwareTeam] });
224
+ const result = await app.runTeam('software-team', { input: 'Implement the change.' });
225
+ ```
226
+
227
+ Delegation and parallel step IDs are durable identities and must remain stable
228
+ within a team version. Team runs emit `team.*` events and relay child events
229
+ with `teamDelegationId` and `teamAgentName`. `startTeam`, `streamTeam`,
230
+ `getTeamRun`, `resumeRun`, `recoverRun` and `cancelRun` use the same durable
231
+ store as agents and workflows.
232
+
233
+ This release intentionally omits automatic routers, network/graph modes,
234
+ quorum, shared blackboards and nested teams. Coordination remains explicit
235
+ TypeScript and shared context travels through task inputs, outputs and the
236
+ existing memory contracts.
237
+
105
238
  ## Connect A Model
106
239
 
107
240
  Real providers connect through the small `ModelGateway` contract:
108
241
 
109
242
  ```ts
110
- import type { ModelGateway } from 'fevex/models';
243
+ import type { ModelGateway } from '@fevex/core/models';
111
244
 
112
245
  export const model: ModelGateway = {
113
- async generate({ messages, tools, outputSchema, signal }) {
114
- const response = await yourProvider.generate({
115
- messages,
116
- tools,
117
- outputSchema,
118
- signal,
119
- });
120
-
121
- return {
122
- output: response.output,
123
- toolCalls: response.toolCalls,
124
- usage: response.usage,
125
- };
246
+ async *stream(input) {
247
+ for await (const event of yourProvider.stream(input)) {
248
+ if (event.type === 'text.delta') {
249
+ yield { type: 'output.delta', delta: event.text };
250
+ } else if (event.type === 'completed') {
251
+ yield {
252
+ type: 'completed',
253
+ result: {
254
+ output: event.output,
255
+ toolCalls: event.toolCalls,
256
+ usage: event.usage,
257
+ providerState: event.providerState,
258
+ },
259
+ };
260
+ }
261
+ }
126
262
  },
127
263
  };
128
264
  ```
129
265
 
266
+ The stream must emit visible text or JSON fragments as `output.delta` followed
267
+ by exactly one `completed` event. Fevex rejects missing or duplicate terminals,
268
+ post-terminal events and a final output that does not match the accumulated
269
+ deltas.
270
+
271
+ `providerState` is opaque, provider-specific continuation data. Fevex carries
272
+ it between model steps within one run and never adds it to agent messages,
273
+ events or `RunResult`. A direct `ModelGateway` consumer must pass it unchanged
274
+ to the next call; it is ephemeral and should not be inspected or persisted.
275
+
130
276
  Provider integrations remain outside the core package. The first official
131
277
  adapter is `@fevex/openai`, authored by Fevex over the native OpenAI HTTP API:
132
278
 
@@ -147,39 +293,94 @@ import { createDeepSeek } from '@fevex/deepseek';
147
293
 
148
294
  const model = createDeepSeek({
149
295
  apiKey: process.env.DEEPSEEK_API_KEY!,
150
- })('deepseek-chat');
296
+ schemaPolicy: 'best-effort',
297
+ })('deepseek-v4-flash');
151
298
  ```
152
299
 
153
300
  Applications can still implement `ModelGateway` directly as shown above.
154
301
 
302
+ Official adapters preserve ordinary model text exactly. JSON-looking text is
303
+ still a string unless `outputSchema` is present; structured output is then
304
+ parsed as JSON and validated locally by Fevex. Tool names must match
305
+ `[A-Za-z0-9_-]{1,64}`.
306
+
307
+ `modelOptions` remains a provider escape hatch, but Fevex-owned request fields
308
+ take precedence: model, messages, tools, tool choice, schema format,
309
+ parallelism and preventive token limits cannot be replaced. Compatible options
310
+ such as temperature, metadata and provider-only reasoning settings are kept.
311
+
155
312
  ## Validation
156
313
 
314
+ `createFevex` validates the complete composition before returning the app,
315
+ including JavaScript values that bypass TypeScript. Invalid registrations throw
316
+ `FevexConfigurationError` with a stable `code`, while preserving a readable
317
+ message.
318
+
319
+ Codes follow one rule: `INVALID_CONFIG` covers the shape of the root config
320
+ object, and every registered entity gets its own code.
321
+
322
+ | Entity | Invalid | Duplicated by name | Referenced but missing |
323
+ |---|---|---|---|
324
+ | Model | `INVALID_MODEL` | — | `MISSING_MODEL` |
325
+ | Agent | `INVALID_AGENT` | `DUPLICATE_AGENT` | — |
326
+ | Tool | `INVALID_TOOL` | `DUPLICATE_TOOL` | `MISSING_TOOL` |
327
+ | Workflow | `INVALID_WORKFLOW` | `DUPLICATE_WORKFLOW` | — |
328
+ | Team | `INVALID_TEAM` | `DUPLICATE_TEAM` | — |
329
+ | Context provider | `INVALID_CONTEXT_PROVIDER` | `DUPLICATE_CONTEXT_PROVIDER` | — |
330
+ | Connection | `INVALID_CONNECTION` | — | — |
331
+ | Policy | `INVALID_POLICY` | — | — |
332
+
333
+ Connections have no duplicate code of their own: two connections sharing a name
334
+ produce colliding `<connection>__<tool>` entries and fail with
335
+ `DUPLICATE_TOOL`.
336
+
337
+ The app captures a shallow snapshot of registered definitions, tool lists,
338
+ limits, model options and `onEvent`. Mutating those caller-owned containers
339
+ after `createFevex` does not reconfigure a running app. Model gateways, schemas,
340
+ tool functions and nested provider-option values remain cooperative runtime
341
+ references and are not frozen.
342
+
157
343
  Fevex accepts
158
344
  [`StandardSchemaV1`](https://github.com/standard-schema/standard-schema)
159
345
  validators for:
160
346
 
347
+ - agent, workflow and team input before it is persisted;
161
348
  - tool input before `execute`;
162
349
  - tool output before it is returned to the model;
163
- - final agent output before it is returned to the application.
350
+ - final agent, workflow and team output before it is returned to the application;
351
+ - declared workflow event payloads before a wait is resolved.
164
352
 
165
353
  Schemas may transform values. The transformed result must still be JSON-safe.
166
- The schema passed to `runAgent` takes precedence over the agent schema for that
167
- run. Plain JSON Schema objects are not executable validators and are not
354
+ Schemas belong to agent, workflow or team definitions; requests cannot replace an output
355
+ contract. Plain JSON Schema objects are not executable validators and are not
168
356
  accepted by these fields.
169
357
 
170
358
  When a model adapter needs schemas, Fevex converts authoring schemas that also
171
359
  implement Standard JSON Schema into JSON Schema 2020-12 before calling
172
- `ModelGateway.generate`. Adapters receive transportable JSON Schema in
173
- `ToolSpec.inputSchema` and `ModelGenerateInput.outputSchema`; they never receive
360
+ `ModelGateway.stream`. Adapters receive transportable JSON Schema in
361
+ `ToolSpec.inputSchema` and `ModelInput.outputSchema`; they never receive
174
362
  the application's executable validators. If a run needs a schema at the model
175
363
  boundary and the schema cannot be converted, Fevex fails before invoking the
176
364
  model with `SCHEMA_NOT_TRANSPORTABLE`.
177
365
 
366
+ Transportable does not mean supported by every provider. Official adapters use
367
+ `schemaPolicy: "strict"` by default and validate their provider's documented
368
+ subset before making an HTTP request. Unsupported schemas fail with
369
+ `PROVIDER_SCHEMA_UNSUPPORTED`. Use `schemaPolicy: "best-effort"` explicitly
370
+ when local validation is sufficient and provider-level schema adherence is not
371
+ required; invalid model output can still fail locally and may require a retry
372
+ owned by the caller.
373
+
374
+ | Boundary | OpenAI strict | DeepSeek strict | Best effort |
375
+ | --- | --- | --- | --- |
376
+ | Tool input | Structured Outputs subset | Strict tools subset on `/beta` | Forwarded without provider guarantee |
377
+ | Final output | Structured Outputs subset | Unsupported | JSON instruction plus local validation |
378
+
178
379
  Use `testModelGateway` to verify a custom model adapter against the shared
179
380
  contract:
180
381
 
181
382
  ```ts
182
- import { testModelGateway } from 'fevex/testing';
383
+ import { testModelGateway } from '@fevex/core/testing';
183
384
 
184
385
  await testModelGateway(model);
185
386
  ```
@@ -204,42 +405,559 @@ The MVP emits:
204
405
 
205
406
  ```text
206
407
  run.started
408
+ model.started
409
+ model.output.delta
207
410
  model.completed
411
+ tool.started
208
412
  tool.completed
209
413
  tool.failed
210
414
  run.completed
211
415
  run.failed
416
+ run.cancelled
417
+ ```
418
+
419
+ `streamAgent` yields each persisted event as it happens. Use
420
+ `model.output.delta` to render output incrementally:
421
+
422
+ ```ts
423
+ for await (const event of app.streamAgent('support', { input: 'Hello' })) {
424
+ if (event.type === 'model.output.delta') {
425
+ process.stdout.write(event.payload.delta);
426
+ }
427
+ }
212
428
  ```
213
429
 
214
430
  Successful runs also return their events in `RunResult.events`.
431
+ Every event has a unique `id` and a run-local `sequence` starting at `1`.
432
+ `model.completed` includes the current step and accumulated usage.
433
+ Tool events include the current step, tool call ID and tool name.
434
+ `run.completed` includes the final JSON output and accumulated usage.
435
+
436
+ `onEvent` is a best-effort observer isolated from execution. Its exceptions do
437
+ not fail the run, and it continues receiving later events. Fevex does not await
438
+ a Promise returned by the observer, but absorbs its rejection to avoid an
439
+ unhandled rejection.
440
+
441
+ Consume the same execution incrementally with `streamAgent`:
442
+
443
+ ```ts
444
+ for await (const event of app.streamAgent('support', {
445
+ input: 'What is the status of account 42?',
446
+ })) {
447
+ console.log(event.type, event.payload);
448
+ }
449
+ ```
450
+
451
+ The stream emits `run.failed` before propagating the original error.
452
+ Cancellation emits `run.cancelled` with a safe `aborted` or `timeout` reason,
453
+ then propagates the original abort reason. `streamAgent` applies backpressure:
454
+ the run advances only while the consumer requests events, and abandoning the
455
+ iterable cancels and persists that local execution.
456
+
457
+ ## Sessions and consultable runs
458
+
459
+ Every run belongs to a session. Omit `sessionId` to create one, then pass the
460
+ returned ID to continue the conversation:
461
+
462
+ ```ts
463
+ const first = await app.runAgent('support', {
464
+ input: 'What is the status of account 42?',
465
+ });
466
+
467
+ const second = await app.runAgent('support', {
468
+ input: 'What should I do next?',
469
+ sessionId: first.sessionId,
470
+ });
471
+ ```
472
+
473
+ Only completed interactions enter session history. Failed and cancelled runs do
474
+ not affect the next prompt. Replace old history with an application-provided
475
+ summary when needed:
476
+
477
+ ```ts
478
+ await app.compactSession(first.sessionId, 'Account 42 is active.');
479
+ ```
480
+
481
+ Use `startAgent` when execution must continue without consuming a stream:
482
+
483
+ ```ts
484
+ const started = await app.startAgent('support', {
485
+ input: 'Check account 42.',
486
+ });
487
+
488
+ const run = await app.getRun(started.id);
489
+ const events = await app.listEvents(started.id);
490
+ const laterEvents = await app.listEvents(started.id, {
491
+ after: events.at(-1)?.id,
492
+ });
493
+
494
+ await app.cancelRun(started.id);
495
+ ```
496
+
497
+ `AgentRun`, `Session` and events are serializable snapshots. `listEvents` orders
498
+ by `sequence`; `after` accepts an event ID from the same run as a stable cursor.
499
+ Unknown cursors fail instead of silently replaying events.
500
+
501
+ `createFevex` uses `InMemoryRunStore` by default. Supply `runStore` to replace
502
+ it. The default survives only while the process lives, and one run at a time
503
+ may update a session.
504
+
505
+ The built-in durable stores reserve each session atomically when creating an
506
+ agent, workflow or team run. A competing run receives `RUN_CONFLICT` before
507
+ calling its model or tools. A paused or orphaned run keeps its reservation:
508
+ resume, recover or cancel that run before starting another. Lease expiry permits
509
+ recovery of the same run; it does not free its conversation for a new run.
510
+
511
+ `Session.revision` is store-managed. Creation, history commits and compaction
512
+ check the snapshot revision and advance it atomically. Compaction rejects active
513
+ or paused sessions and stale snapshots. On conflict, reload the session and
514
+ decide whether to retry; never automatically retry external effects.
515
+
516
+ Custom durable adapters must implement these guarantees in `createExecution`,
517
+ `commitExecution` and `saveSession`, and pass `testRunStore`.
518
+ `ExecutionCreate.session` is now required for existing sessions as well as new
519
+ ones. A legacy session without a revision is treated as revision zero.
520
+ The low-level `saveRun` API is for trusted storage maintenance; it must not be
521
+ used to bypass runtime reservation or overwrite active runs.
522
+
523
+ Upgrade all workers and adapters together; old workers do not enforce session
524
+ reservation. Every durable commit also validates the lease owner, generation and expiry in
525
+ the same transaction as the run revision. Acquisition advances the generation,
526
+ including when an owner ID is reused. Release expires the lease without deleting
527
+ that counter; stale workers cannot commit, renew or release a newer generation.
528
+
529
+ Custom adapters must accept `ExecutionCommit.lease` and the generation argument
530
+ to `releaseLease`. Initialize `RunLease.generation` to zero for creation or
531
+ acquisition and retain the generation assigned by the store on success.
532
+ Renewal keeps the same generation and cannot revive an expired lease.
533
+
534
+ Renewal errors surface as `FevexRunError` with code `RUN_CONFLICT` and the original
535
+ error in `cause`. Local cleanup does not persist cancellation after ownership is lost.
536
+ A lost, failed or blocked renewal aborts local execution; the runtime will not
537
+ start further work or persist results under that token. The orphaned run remains
538
+ available for recovery. External effects already in flight still need
539
+ idempotency or fencing in the external system.
540
+
541
+ Migrate storage before restarting workers. Legacy leases get generation zero
542
+ and cannot commit or renew; recovery can acquire a new generation after they
543
+ expire. Never run old and new workers together during this upgrade.
544
+
545
+ ## HTTP protocol
546
+
547
+ `@fevex/core/http` exposes the runtime through versioned JSON and SSE using
548
+ native Fetch types:
549
+
550
+ ```ts
551
+ import { createFevexHttpHandler, createFevexHttpClient } from '@fevex/core/http';
552
+
553
+ const handler = createFevexHttpHandler({ fevex: app });
554
+ const client = createFevexHttpClient({ baseUrl: 'http://localhost:3001' });
555
+
556
+ const run = await client.startRun('support', { input: 'Check account 42.' });
557
+ for await (const event of client.observeRun(run.id)) {
558
+ console.log(event.type);
559
+ }
560
+ ```
561
+
562
+ Protocol v3 also exposes `client.startWorkflow(...)` and
563
+ `client.startTeam(...)`; all run kinds share observation, resume, recovery and
564
+ cancellation endpoints.
565
+
566
+ Hosts pass authenticated `ExecutionContext` separately to `handler(request,
567
+ { context })`. Observation reconnects with `Last-Event-ID`; disconnecting an
568
+ SSE response never cancels its run. Cancellation is explicit through
569
+ `client.cancelRun(run.id)`.
570
+
571
+ ## Approvals and durable runs
572
+
573
+ Tools can declare risk, approval, idempotency, retries and credential names.
574
+ Approval pauses before credentials are resolved or the effect starts:
575
+
576
+ ```ts
577
+ const charge = defineTool({
578
+ name: 'charge',
579
+ risk: 'sensitive',
580
+ approval: 'required',
581
+ idempotency: 'keyed',
582
+ credentials: ['payments-api-key'],
583
+ retry: { maxAttempts: 3, backoffMs: 200, maxBackoffMs: 2_000 },
584
+ async execute(input, context) {
585
+ const apiKey = await context.getCredential('payments-api-key');
586
+ return chargeCustomer(input, {
587
+ apiKey,
588
+ idempotencyKey: context.idempotencyKey,
589
+ });
590
+ },
591
+ });
592
+ ```
593
+
594
+ `runAgent` throws `RunPausedError`, `streamAgent` emits `run.paused`, and
595
+ `startAgent` leaves the paused snapshot consultable. Continue an approval from
596
+ any runtime sharing the durable store:
597
+
598
+ ```ts
599
+ await app.resumeRun(runId, {
600
+ type: 'approval',
601
+ approvalId,
602
+ decision: 'approve',
603
+ actor: { id: 'reviewer-42' },
604
+ });
605
+ ```
606
+
607
+ Use `InMemoryRunStore` for ephemeral runs, `@fevex/sqlite` for local durability,
608
+ or `@fevex/postgres` across multiple processes:
609
+
610
+ ```ts
611
+ import { createSQLiteRunStore } from '@fevex/sqlite';
612
+
613
+ const runStore = createSQLiteRunStore({
614
+ filename: '.fevex/runs.db',
615
+ });
616
+ ```
617
+
618
+ SQLite creates and migrates its local file automatically. PostgreSQL migrations
619
+ remain explicit and are intended for server deployments.
620
+
621
+ Policies run before approval resolution, credentials and each effect or retry.
622
+ Credential values are not stored or sent to the model, and tool outputs that
623
+ contain a resolved value are rejected.
624
+
625
+ ## Limits
626
+
627
+ Agent runs default to 8 model steps and 16 sequential tool calls. Override
628
+ them on the agent:
629
+
630
+ ```ts
631
+ defineAgent({
632
+ name: 'support',
633
+ instructions: 'Answer account questions clearly.',
634
+ limits: {
635
+ maxSteps: 4,
636
+ maxToolCalls: 6,
637
+ maxInputTokens: 10_000,
638
+ maxOutputTokens: 2_000,
639
+ },
640
+ });
641
+ ```
642
+
643
+ Input token limits are cumulative run budgets checked from provider usage after
644
+ each model response. Output token limits are also sent to model gateways as the
645
+ remaining preventive cap, then verified from reported usage. A configured token
646
+ limit fails explicitly when the gateway does not report the corresponding usage
647
+ field. Set a token limit to `false` to leave it unlimited.
648
+
649
+ Cancellation is immediate from the caller's perspective. Models, tools and
650
+ schema validators still need to honor the supplied `AbortSignal` to stop their
651
+ underlying work. External systems must enforce the supplied idempotency key to
652
+ make keyed retries safe. Repeated tool call IDs are rejected within a run before
653
+ Fevex repeats an effect.
654
+
655
+ ## Observability
656
+
657
+ Add trace exporters to the application configuration; agent definitions stay
658
+ unchanged:
659
+
660
+ ```ts
661
+ import { createOpenTelemetryExporter } from '@fevex/opentelemetry';
662
+
663
+ const app = createFevex({
664
+ models,
665
+ agents,
666
+ tools,
667
+ observability: {
668
+ exporters: [createOpenTelemetryExporter()],
669
+ calculateCost({ usage }) {
670
+ return {
671
+ amount: (usage.totalTokens ?? 0) * 0.000_001,
672
+ currency: 'USD',
673
+ };
674
+ },
675
+ },
676
+ });
677
+
678
+ await app.runAgent('support', { input: 'Hello' });
679
+ await app.flushObservability();
680
+ ```
681
+
682
+ Traces contain only metadata by default. Output and safe error capture require
683
+ an explicit `content.include` policy; sensitive object keys are redacted before
684
+ an optional application redactor runs. Exporters run outside the run path: their
685
+ errors never change the run result, and `flushObservability()` reports pending
686
+ export failures. OpenTelemetry SDK setup, OTLP export and shutdown remain
687
+ application responsibilities.
688
+
689
+ ## Evaluations
690
+
691
+ `@fevex/core/evals` runs local datasets sequentially with fresh sessions:
692
+
693
+ ```ts
694
+ import {
695
+ exactOutputScorer,
696
+ runEvaluation,
697
+ toolSelectionScorer,
698
+ } from '@fevex/core/evals';
699
+
700
+ const report = await runEvaluation({
701
+ app,
702
+ suiteName: 'support-regression',
703
+ targetVersion: 'v2',
704
+ agentName: 'support',
705
+ dataset: {
706
+ name: 'support',
707
+ version: '1',
708
+ cases: [{
709
+ id: 'active-account',
710
+ input: 'Check account 42',
711
+ expected: {
712
+ output: { answer: 'Account 42 is active.' },
713
+ tools: ['accounts_get'],
714
+ },
715
+ }],
716
+ },
717
+ scorers: [exactOutputScorer(), toolSelectionScorer()],
718
+ });
719
+ ```
720
+
721
+ Built-in deterministic scorers cover exact output, ordered or unordered tool
722
+ selection, forbidden tools, max latency, max tokens and max cost. Use
723
+ `serializeEvalReport` for a canonical, versionable JSON baseline, `jsonReporter`
724
+ or `textReporter` with your own write callback, and `compareEvaluationReports`
725
+ to fail CI when cases disappear, passing scores fail or scores drop beyond the
726
+ configured tolerance.
727
+
728
+ ## Connections
729
+
730
+ Connections turn remote tool providers into normal Fevex tools. Names are local
731
+ and explicit: a connection named `docs` with remote tool `search` is exposed to
732
+ agents as `docs__search`.
733
+
734
+ ```ts
735
+ import { createFevex, defineAgent, defineConnection } from '@fevex/core';
736
+ import { createMcpToolProvider } from '@fevex/mcp';
737
+
738
+ const docs = defineConnection({
739
+ name: 'docs',
740
+ provider: createMcpToolProvider({ url: 'https://example.com/mcp' }),
741
+ allowlist: ['search'],
742
+ timeoutMs: 30_000,
743
+ tools: {
744
+ search: {
745
+ description: 'Search docs.',
746
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
747
+ },
748
+ },
749
+ });
750
+
751
+ createFevex({
752
+ models: {},
753
+ agents: [defineAgent({
754
+ name: 'assistant',
755
+ instructions: 'Help.',
756
+ tools: ['docs__search'],
757
+ })],
758
+ connections: [docs],
759
+ });
760
+ ```
761
+
762
+ Fevex owns allowlists, timeouts, approvals, policies and safe error messages.
763
+ MCP metadata and annotations are treated as untrusted hints. The first MCP
764
+ adapter supports Streamable HTTP only; `stdio`, resources, prompts and legacy
765
+ HTTP+SSE stay out of scope.
766
+
767
+ OpenAPI providers use the same connection contract:
768
+
769
+ ```ts
770
+ import { createOpenApiToolProvider } from '@fevex/openapi';
771
+
772
+ const billing = defineConnection({
773
+ name: 'billing',
774
+ provider: createOpenApiToolProvider({
775
+ document: openApiDocument,
776
+ operations: { allow: ['getInvoice'] },
777
+ headers: () => ({ Authorization: `Bearer ${process.env.BILLING_TOKEN}` }),
778
+ }),
779
+ allowlist: ['getInvoice'],
780
+ });
781
+ ```
782
+
783
+ `@fevex/openapi` accepts bundled JSON OpenAPI 3.1.x documents, local refs and
784
+ JSON request/response bodies. It rejects unsupported JSON Schema keywords,
785
+ remote refs, YAML, OpenAPI 3.0/3.2 and non-JSON payloads instead of silently
786
+ dropping constraints.
787
+
788
+ ## Channels
789
+
790
+ `@fevex/core/channels` provides a small adapter contract and `handleChannelInput`
791
+ for hosting apps that receive messages from another surface:
792
+
793
+ ```ts
794
+ import { handleChannelInput } from '@fevex/core/channels';
795
+
796
+ const sessions = new Map<string, string>();
797
+
798
+ const result = await handleChannelInput(
799
+ { id: 'm1', conversationId: 'c1', text: 'hello' },
800
+ {
801
+ fevex: app,
802
+ agentName: 'assistant',
803
+ adapter: {
804
+ name: 'memory',
805
+ async parse(input) {
806
+ return {
807
+ id: input.id,
808
+ deliveryId: input.id,
809
+ conversationId: input.conversationId,
810
+ content: input.text,
811
+ };
812
+ },
813
+ async deliver(output) {
814
+ return output;
815
+ },
816
+ },
817
+ resolveSessionId(message) {
818
+ return sessions.get(message.conversationId);
819
+ },
820
+ },
821
+ );
822
+
823
+ if (!result.ignored) sessions.set(result.message.conversationId, result.run.sessionId);
824
+ ```
825
+
826
+ The helper emits local channel events around the run. Slack verification,
827
+ acknowledgement, durable redelivery and deduplication remain outside this core
828
+ helper and belong to the Slack adapter phase.
829
+
830
+ ## Knowledge and memory
831
+
832
+ `@fevex/core/knowledge` keeps external context separate from sessions:
833
+
834
+ ```ts
835
+ import {
836
+ InMemoryMemoryStore,
837
+ createFevex,
838
+ defineAgent,
839
+ defineContextProvider,
840
+ defineSkill,
841
+ } from '@fevex/core';
842
+
843
+ const app = createFevex({
844
+ models,
845
+ memoryStore: new InMemoryMemoryStore(),
846
+ contextProviders: [
847
+ defineSkill({
848
+ name: 'refunds',
849
+ instructions: 'Refunds are allowed for 30 days after purchase.',
850
+ }),
851
+ defineContextProvider({
852
+ name: 'account',
853
+ async read({ context }) {
854
+ return [{ id: 'tier', content: `Plan: ${context?.attributes?.plan ?? 'free'}` }];
855
+ },
856
+ }),
857
+ ],
858
+ agents: [
859
+ defineAgent({
860
+ name: 'support',
861
+ instructions: 'Answer with business context.',
862
+ skills: ['refunds'],
863
+ context: ['account'],
864
+ memory: { read: true, write: true },
865
+ }),
866
+ ],
867
+ });
868
+ ```
869
+
870
+ Context providers are lazy per run. Memory is a replaceable store; the built-in
871
+ store is in-memory substring search, not embeddings, vector search or durable
872
+ storage.
215
873
 
216
874
  ## Public Exports
217
875
 
218
876
  | Import | Purpose |
219
877
  | --- | --- |
220
- | `fevex` | Common runtime, definitions and contracts |
221
- | `fevex/agents` | Agent definitions |
222
- | `fevex/models` | Provider-neutral model contracts |
223
- | `fevex/tools` | Tool definitions and execution context |
224
- | `fevex/runtime` | Run request and result contracts |
225
- | `fevex/testing` | Deterministic testing helpers |
226
-
227
- All subpaths ship in the same `fevex` package.
878
+ | `@fevex/core` | Common runtime, definitions and contracts |
879
+ | `@fevex/core/agents` | Agent definitions |
880
+ | `@fevex/core/channels` | Channel adapters and message handling |
881
+ | `@fevex/core/knowledge` | Context providers, skills and memory contracts |
882
+ | `@fevex/core/models` | Provider-neutral model contracts |
883
+ | `@fevex/core/tools` | Tool, connection and provider contracts |
884
+ | `@fevex/core/workflows` | Durable workflow definitions |
885
+ | `@fevex/core/teams` | Multiagent team definitions |
886
+ | `@fevex/core/runtime` | Runs, sessions and store contracts |
887
+ | `@fevex/core/sandbox` | Sandbox contract and local development sandbox |
888
+ | `@fevex/core/policies` | Authorization policy contracts |
889
+ | `@fevex/core/observability` | Trace, redaction and cost contracts |
890
+ | `@fevex/core/evals` | Datasets, scorers, reporters and regressions |
891
+ | `@fevex/core/testing` | Deterministic testing helpers |
892
+
893
+ All subpaths ship in the same `@fevex/core` package.
894
+
895
+ ## Source Layout
896
+
897
+ Each public subpath is a folder under `src`. Its `index.ts` is intentionally a
898
+ small export barrel; contracts and implementations live in files named after
899
+ their responsibility. For example, `runtime/run-store.ts` owns persistence
900
+ contracts while `runtime/in-memory-run-store.ts` owns the development
901
+ implementation. Keep provider-neutral public contracts in their domain folder
902
+ and orchestration details under `src/internal`.
903
+
904
+ When adding a feature:
905
+
906
+ - extend an existing responsibility before creating a new abstraction;
907
+ - keep platform or provider-specific behavior outside `@fevex/core`;
908
+ - export public API only through the domain barrel and preserve existing
909
+ subpath imports;
910
+ - document lifecycle, persistence, security and replay semantics; avoid
911
+ comments that only restate the TypeScript signature;
912
+ - add the smallest test that exercises the behavior at its public boundary.
228
913
 
229
914
  ## Current Scope
230
915
 
231
916
  The MVP intentionally supports:
232
917
 
233
- - one model response, or one model-tool-model cycle;
234
- - an in-memory runtime;
235
- - synchronous event observers;
918
+ - bounded multi-step model-tool loops with sequential tool execution;
919
+ - native provider streaming and durable, consultable runs;
920
+ - synchronous event observers and `AsyncIterable` event streaming;
921
+ - local traces, OpenTelemetry export and deterministic evaluation suites;
236
922
  - optional Standard Schema validation;
237
- - `AbortSignal` cancellation.
923
+ - immediate `AbortSignal` cancellation with cooperative underlying work.
238
924
 
239
- It does not yet include streaming, retries, persistence, approvals, a CLI or
240
- official provider adapters. `AgentDefinition.limits` is reserved and has no
241
- runtime effect yet.
925
+ It does not include a CLI, hosted observability service, model-judge evals or
926
+ human evaluation workflows.
242
927
 
243
928
  ## License
244
929
 
245
930
  Apache-2.0
931
+
932
+ ### Connection discovery isolation
933
+
934
+ Connection tool metadata is discovered for each resolution using that caller's
935
+ context. The core does not cache catalogs or rejected discovery promises across
936
+ calls. A failed discovery can therefore recover on the next attempt without
937
+ recreating the runtime. Providers must scope their own state to an identity;
938
+ `@fevex/mcp` binds each instance to its first resolved header set and rejects
939
+ changes. Use a separate MCP provider/connection per identity and recreate it when
940
+ credentials rotate. This adds discovery requests in exchange for avoiding stale
941
+ or cross-identity catalogs. Tool calls are not automatically replayed by MCP.
942
+
943
+ ### Paged event reads and SSE
944
+
945
+ `listEvents(runId, { after, limit, order })` returns events whose sequence is
946
+ strictly greater than the cursor's sequence. `order` is `asc` by default;
947
+ `{ order: 'desc', limit: 1 }` retrieves the latest event. `limit` must be a positive
948
+ safe integer. Omitting it preserves the existing complete-result behavior.
949
+ Unknown cursors and cursors belonging to another run reject with
950
+ `FevexRunError` code `INVALID_CURSOR` (HTTP 400).
951
+
952
+ SQLite and PostgreSQL resolve the cursor by ID and filter, order and limit in
953
+ SQL using the existing `(run_id, sequence)` index. Prior event payloads are not
954
+ loaded to find the cursor. Custom run stores must implement these options and
955
+ the cursor error contract before being used with the updated HTTP handler.
956
+
957
+ The HTTP handler accepts `eventPageSize` (default 100, range 1–1000). It uses this
958
+ limit for the initial SSE read and every subsequent read. Full pages drain
959
+ without the polling delay; short pages wait for `pollIntervalMs`. Stream demand
960
+ controls progress, so a slow consumer retains at most one page plus the stream's
961
+ single queued frame, rather than buffering the run's entire history. Disconnecting
962
+ cancels polling. This bounds event counts, not the byte size of an individual event;
963
+ per-event payload limits and data retention remain host responsibilities.