@fevex/core 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (56) hide show
  1. package/README.md +786 -13
  2. package/dist/agents/index.d.mts +2 -2
  3. package/dist/agents/index.mjs +1 -1
  4. package/dist/artifacts/index.d.mts +2 -0
  5. package/dist/artifacts/index.mjs +2 -0
  6. package/dist/artifacts-gUzemET7.mjs +440 -0
  7. package/dist/channels/index.d.mts +2 -0
  8. package/dist/channels/index.mjs +192 -0
  9. package/dist/evals/index.d.mts +19 -11
  10. package/dist/evals/index.mjs +125 -122
  11. package/dist/fevex-_KOsOsB8.d.mts +129 -0
  12. package/dist/http/index.d.mts +150 -0
  13. package/dist/http/index.mjs +692 -0
  14. package/dist/index-26ilbzAs.d.mts +50 -0
  15. package/dist/index-B8EcvWeN.d.mts +103 -0
  16. package/dist/index-BEqYWean.d.mts +118 -0
  17. package/dist/index-Btth7rdL.d.mts +58 -0
  18. package/dist/{index-QTkd6a7C.d.mts → index-CFzrvDQ0.d.mts} +4 -3
  19. package/dist/index-CIHWc5V1.d.mts +116 -0
  20. package/dist/index-D7r0puBi.d.mts +64 -0
  21. package/dist/index-DHcfIfh-.d.mts +224 -0
  22. package/dist/index-DUhNa7IR.d.mts +73 -0
  23. package/dist/index-lCCFokc6.d.mts +617 -0
  24. package/dist/index.d.mts +22 -10
  25. package/dist/index.mjs +4178 -671
  26. package/dist/knowledge/index.d.mts +2 -0
  27. package/dist/knowledge/index.mjs +289 -0
  28. package/dist/model-stream-JQdHDDcj.mjs +131 -0
  29. package/dist/models/index.d.mts +2 -46
  30. package/dist/observability/index.d.mts +2 -2
  31. package/dist/policies/index.d.mts +1 -1
  32. package/dist/policies/index.mjs +1 -1
  33. package/dist/run-error-Cr17wPGH.mjs +23 -0
  34. package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
  35. package/dist/runtime/index.d.mts +2 -2
  36. package/dist/runtime/index.mjs +271 -9
  37. package/dist/sandbox/index.d.mts +2 -0
  38. package/dist/sandbox/index.mjs +147 -0
  39. package/dist/teams/index.d.mts +2 -0
  40. package/dist/teams/index.mjs +2 -0
  41. package/dist/teams-Ctz676G4.mjs +82 -0
  42. package/dist/testing/index.d.mts +39 -9
  43. package/dist/testing/index.mjs +512 -28
  44. package/dist/tools/index.d.mts +3 -3
  45. package/dist/tools/index.mjs +442 -2
  46. package/dist/types-CckDlhYo.d.mts +575 -0
  47. package/dist/workflows/index.d.mts +2 -0
  48. package/dist/workflows/index.mjs +6 -0
  49. package/package.json +31 -3
  50. package/dist/exact-definition-NHBBFlFR.d.mts +0 -4
  51. package/dist/fevex-Dz6GBz9o.d.mts +0 -32
  52. package/dist/index-BzxHfyYV.d.mts +0 -23
  53. package/dist/index-CZGyhNIQ.d.mts +0 -47
  54. package/dist/index-C_Zpr13f.d.mts +0 -124
  55. package/dist/index-DGWOGXzc.d.mts +0 -181
  56. package/dist/index-ojCY3lwm.d.mts +0 -79
package/README.md CHANGED
@@ -28,16 +28,28 @@ adapters, validation and tests without adopting a heavy runtime.
28
28
  npm install @fevex/core
29
29
  ```
30
30
 
31
- For official model adapters:
31
+ The npm release pipeline publishes exactly three official packages:
32
32
 
33
33
  ```bash
34
34
  npm install @fevex/core @fevex/openai
35
35
  npm install @fevex/core @fevex/deepseek
36
- npm install @fevex/core @fevex/sqlite
37
- npm install @fevex/core @fevex/postgres
38
- npm install @fevex/core @fevex/opentelemetry @opentelemetry/api
39
36
  ```
40
37
 
38
+ CI builds, typechecks, tests and tarball-smokes all eleven 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/anthropic` | Anthropic Messages API model adapter |
45
+ | `@fevex/mcp` | Model Context Protocol tools and connections |
46
+ | `@fevex/openapi` | Tools generated from an OpenAPI document |
47
+ | `@fevex/sqlite` | SQLite run and session store |
48
+ | `@fevex/postgres` | PostgreSQL run and session store |
49
+ | `@fevex/opentelemetry` | OpenTelemetry trace and metric export |
50
+ | `@fevex/browser` | Browser automation tools |
51
+ | `@fevex/sandbox-docker` | Docker-backed production sandbox provider |
52
+
41
53
  Schemas are optional. The examples use Zod 4, but any Standard
42
54
  Schema-compatible validator can be used:
43
55
 
@@ -45,6 +57,20 @@ Schema-compatible validator can be used:
45
57
  npm install zod
46
58
  ```
47
59
 
60
+ ## Compatibility & releases
61
+
62
+ FEVEX is pre-1.0. Documented imports from official packages are the public API;
63
+ `src/internal`, undocumented paths and checkpoint shapes are not. While in alpha,
64
+ `minor` releases may contain breaking changes with migration notes; `patch`
65
+ releases stay compatible. Store migrations are forward-only, checkpoints are
66
+ versioned and private, and downgrades are not supported.
67
+
68
+ Only the latest alpha is supported; pin exact versions in production. Releases
69
+ come from reviewed changesets, publish only the three official packages, and
70
+ place alpha versions on the npm `latest` tag. Read the full
71
+ [compatibility and release policy](https://fevex.dev/docs/compatibility/) before
72
+ upgrading a durable deployment.
73
+
48
74
  ## Quickstart
49
75
 
50
76
  This example executes a complete model-tool-model cycle without network access:
@@ -106,6 +132,133 @@ console.log(result.output.answer);
106
132
  `fakeModel` returns its configured responses in order and records every input in
107
133
  `model.calls`.
108
134
 
135
+ ## Durable workflows
136
+
137
+ Workflows require a `DurableRunStore`. Their definition owns the input, output,
138
+ event and limit contracts:
139
+
140
+ ```ts
141
+ const review = defineWorkflow({
142
+ name: 'review',
143
+ version: '2',
144
+ inputSchema: z.object({ draft: z.string() }),
145
+ outputSchema: z.string(),
146
+ events: {
147
+ 'review.approved': {
148
+ payloadSchema: z.object({ approved: z.literal(true) }),
149
+ requireActor: true,
150
+ },
151
+ },
152
+ limits: { maxSteps: 8, maxToolCalls: 12 },
153
+ async run(step, input) {
154
+ const draft = await step.agent('draft', 'writer', { input });
155
+ const approval = await step.waitForEvent('approval', 'review.approved');
156
+ return `${draft.output} (${approval.actor?.id}, ${approval.receivedAt})`;
157
+ },
158
+ });
159
+ ```
160
+
161
+ Input is validated and transformed before the initial checkpoint is stored.
162
+ Output is always validated with the definition schema, including after a pause
163
+ or recovery. Effective workflow limits combine definition and request limits;
164
+ agent steps additionally combine their own agent/request limits. These values
165
+ are checkpointed so recovery cannot silently widen a budget.
166
+
167
+ `stepId` is the durable identity of a step: keep it stable while its meaning is
168
+ stable. Increment `version` when replay order, business rules or step meanings
169
+ change. Completed agent steps are reused on replay and `step.agent` always
170
+ returns `{ runId, sessionId, output, usage? }`, without a transient `events`
171
+ field.
172
+
173
+ Child execution context inherits the workflow context. Namespace, attributes
174
+ and prompt values are merged; an authenticated parent actor cannot be replaced
175
+ by a child. `parallel` waits for every declared task to settle, records durable
176
+ child successes, reports multiple failures in declaration order and resolves
177
+ multiple pauses one at a time. Arbitrary promises used in `parallel` must be
178
+ pure or replay-safe; side effects belong in durable agent/tool steps.
179
+
180
+ External events must be declared. `waitForEvent` returns
181
+ `{ payload, actor, receivedAt }`; payload and required actor are checked before
182
+ the checkpoint changes. Cancelling a workflow first cancels its running or
183
+ paused children. Compensation runs in reverse completed-step order; if both the
184
+ workflow and compensation fail, the terminal error is a stable
185
+ `AggregateError`.
186
+
187
+ Durable executions start with a checkpoint v2 and lease in the same atomic
188
+ store operation. An external worker may recover an orphaned `running` run after
189
+ its lease expires:
190
+
191
+ ```ts
192
+ await app.recoverRun(runId, { actor: { id: 'recovery-worker', type: 'service' } });
193
+ ```
194
+
195
+ Paused runs use `resumeRun`; terminal runs and older checkpoints are rejected.
196
+ Fevex does not poll globally: deployments need an external orphan detector and
197
+ a scheduler that calls `resumeRun` for elapsed timers. Exactly-once protection
198
+ is limited to keyed/idempotent tools and durable steps.
199
+
200
+ ## Advanced teams
201
+
202
+ Teams are an opt-in coordination layer on the durable workflow engine. They
203
+ reuse registered agents and cannot widen an agent's model, tools, policies,
204
+ sandbox or limits:
205
+
206
+ ```ts
207
+ const model = fakeModel(
208
+ { output: 'plan' },
209
+ { output: 'research' },
210
+ { output: 'code' },
211
+ { output: 'approved' },
212
+ );
213
+
214
+ const softwareTeam = defineTeam({
215
+ name: 'software-team',
216
+ version: '1',
217
+ supervisor: 'planner',
218
+ members: [
219
+ { agent: 'researcher', role: 'research' },
220
+ { agent: 'coder', role: 'implementation' },
221
+ { agent: 'reviewer', role: 'review' },
222
+ ],
223
+ limits: { maxDelegations: 12, maxParallel: 2 },
224
+ async run(team, input) {
225
+ const plan = await team.delegate('plan', {
226
+ agent: 'planner',
227
+ task: input,
228
+ expectedOutput: 'Implementation plan',
229
+ });
230
+ const work = await team.parallel('work', {
231
+ research: () =>
232
+ team.delegate('research', { agent: 'researcher', task: plan.output }),
233
+ implementation: () =>
234
+ team.delegate('implementation', { agent: 'coder', task: plan.output }),
235
+ });
236
+ return (
237
+ await team.handoff('review', {
238
+ from: 'coder',
239
+ to: 'reviewer',
240
+ reason: 'Final review',
241
+ task: work,
242
+ })
243
+ ).output;
244
+ },
245
+ });
246
+
247
+ const app = createFevex({ models, agents, teams: [softwareTeam] });
248
+ const result = await app.runTeam('software-team', { input: 'Implement the change.' });
249
+ ```
250
+
251
+ Delegation and parallel step IDs are durable identities and must remain stable
252
+ within a team version. Team runs emit `team.*` events and relay child events
253
+ with `teamDelegationId` and `teamAgentName`. `startTeam`, `streamTeam`,
254
+ `getTeamRun`, `resumeRun`, `recoverRun` and `cancelRun` use the same durable
255
+ store as agents and workflows.
256
+
257
+ This release intentionally omits automatic routers, network/graph modes,
258
+ quorum, shared blackboards and nested teams. Coordination remains explicit
259
+ TypeScript and shared context travels through task inputs, outputs and the
260
+ existing memory contracts.
261
+
109
262
  ## Connect A Model
110
263
 
111
264
  Real providers connect through the small `ModelGateway` contract:
@@ -168,6 +321,20 @@ const model = createDeepSeek({
168
321
  })('deepseek-v4-flash');
169
322
  ```
170
323
 
324
+ Anthropic is available from this repository until it is published:
325
+
326
+ ```ts
327
+ import { createAnthropic } from '@fevex/anthropic';
328
+
329
+ const model = createAnthropic({
330
+ apiKey: process.env.ANTHROPIC_API_KEY!,
331
+ })('claude-opus-5-5');
332
+ ```
333
+
334
+ The Anthropic adapter streams Messages API events, replays native thinking and
335
+ `tool_use` blocks through `providerState`, maps reasoning effort to
336
+ `output_config.effort`, and supports strict JSON outputs and tool schemas.
337
+
171
338
  Applications can still implement `ModelGateway` directly as shown above.
172
339
 
173
340
  Official adapters preserve ordinary model text exactly. JSON-looking text is
@@ -184,8 +351,26 @@ such as temperature, metadata and provider-only reasoning settings are kept.
184
351
 
185
352
  `createFevex` validates the complete composition before returning the app,
186
353
  including JavaScript values that bypass TypeScript. Invalid registrations throw
187
- `FevexConfigurationError` with a stable `code` such as `INVALID_TOOL`,
188
- `DUPLICATE_AGENT` or `MISSING_MODEL`, while preserving a readable message.
354
+ `FevexConfigurationError` with a stable `code`, while preserving a readable
355
+ message.
356
+
357
+ Codes follow one rule: `INVALID_CONFIG` covers the shape of the root config
358
+ object, and every registered entity gets its own code.
359
+
360
+ | Entity | Invalid | Duplicated by name | Referenced but missing |
361
+ |---|---|---|---|
362
+ | Model | `INVALID_MODEL` | — | `MISSING_MODEL` |
363
+ | Agent | `INVALID_AGENT` | `DUPLICATE_AGENT` | — |
364
+ | Tool | `INVALID_TOOL` | `DUPLICATE_TOOL` | `MISSING_TOOL` |
365
+ | Workflow | `INVALID_WORKFLOW` | `DUPLICATE_WORKFLOW` | — |
366
+ | Team | `INVALID_TEAM` | `DUPLICATE_TEAM` | — |
367
+ | Context provider | `INVALID_CONTEXT_PROVIDER` | `DUPLICATE_CONTEXT_PROVIDER` | — |
368
+ | Connection | `INVALID_CONNECTION` | — | — |
369
+ | Policy | `INVALID_POLICY` | — | — |
370
+
371
+ Connections have no duplicate code of their own: two connections sharing a name
372
+ produce colliding `<connection>__<tool>` entries and fail with
373
+ `DUPLICATE_TOOL`.
189
374
 
190
375
  The app captures a shallow snapshot of registered definitions, tool lists,
191
376
  limits, model options and `onEvent`. Mutating those caller-owned containers
@@ -197,13 +382,15 @@ Fevex accepts
197
382
  [`StandardSchemaV1`](https://github.com/standard-schema/standard-schema)
198
383
  validators for:
199
384
 
385
+ - agent, workflow and team input before it is persisted;
200
386
  - tool input before `execute`;
201
387
  - tool output before it is returned to the model;
202
- - final agent output before it is returned to the application.
388
+ - final agent, workflow and team output before it is returned to the application;
389
+ - declared workflow event payloads before a wait is resolved.
203
390
 
204
391
  Schemas may transform values. The transformed result must still be JSON-safe.
205
- The schema passed to `runAgent` takes precedence over the agent schema for that
206
- run. Plain JSON Schema objects are not executable validators and are not
392
+ Schemas belong to agent, workflow or team definitions; requests cannot replace an output
393
+ contract. Plain JSON Schema objects are not executable validators and are not
207
394
  accepted by these fields.
208
395
 
209
396
  When a model adapter needs schemas, Fevex converts authoring schemas that also
@@ -259,6 +446,7 @@ run.started
259
446
  model.started
260
447
  model.output.delta
261
448
  model.completed
449
+ context.compacted
262
450
  tool.started
263
451
  tool.completed
264
452
  tool.failed
@@ -329,6 +517,28 @@ summary when needed:
329
517
  await app.compactSession(first.sessionId, 'Account 42 is active.');
330
518
  ```
331
519
 
520
+ Automatic compaction is opt-in through a context budget:
521
+
522
+ ```ts
523
+ const support = defineAgent({
524
+ name: 'support',
525
+ instructions: 'Answer clearly.',
526
+ contextBudget: { contextWindow: 128_000, reserveOutputTokens: 8_000 },
527
+ });
528
+ ```
529
+
530
+ Before each model call, Fevex estimates the prompt. Above 80% of the usable
531
+ window it replaces older completed turns with a summary, keeps system
532
+ instructions, the current request and the latest tool batch intact, and emits
533
+ `context.compacted`. The summary is cached on the session and reused across
534
+ runs; the original history is not rewritten. Use `compaction.compactor` to
535
+ inject a summarizer; the default is a deterministic digest and
536
+ `createModelContextCompactor` provides a model-backed option. Redact event
537
+ summaries with `compaction.redactSummary` and read counters with
538
+ `app.getCompactionStats()`.
539
+ `limits.maxInputTokens` is still the cumulative spend cap, while
540
+ `contextBudget.contextWindow` is the model context window.
541
+
332
542
  Use `startAgent` when execution must continue without consuming a stream:
333
543
 
334
544
  ```ts
@@ -353,6 +563,97 @@ Unknown cursors fail instead of silently replaying events.
353
563
  it. The default survives only while the process lives, and one run at a time
354
564
  may update a session.
355
565
 
566
+ The built-in durable stores reserve each session atomically when creating an
567
+ agent, workflow or team run. A competing run receives `RUN_CONFLICT` before
568
+ calling its model or tools. A paused or orphaned run keeps its reservation:
569
+ resume, recover or cancel that run before starting another. Lease expiry permits
570
+ recovery of the same run; it does not free its conversation for a new run.
571
+
572
+ `Session.revision` is store-managed. Creation, history commits and compaction
573
+ check the snapshot revision and advance it atomically. Compaction rejects active
574
+ or paused sessions and stale snapshots. On conflict, reload the session and
575
+ decide whether to retry; never automatically retry external effects.
576
+
577
+ Custom durable adapters must implement these guarantees in `createExecution`,
578
+ `commitExecution` and `saveSession`, and pass `testRunStore`.
579
+ `ExecutionCreate.session` is now required for existing sessions as well as new
580
+ ones. A legacy session without a revision is treated as revision zero.
581
+ The low-level `saveRun` API is for trusted storage maintenance; it must not be
582
+ used to bypass runtime reservation or overwrite active runs.
583
+
584
+ Upgrade all workers and adapters together; old workers do not enforce session
585
+ reservation. Every durable commit also validates the lease owner, generation and expiry in
586
+ the same transaction as the run revision. Acquisition advances the generation,
587
+ including when an owner ID is reused. Release expires the lease without deleting
588
+ that counter; stale workers cannot commit, renew or release a newer generation.
589
+
590
+ Custom adapters must accept `ExecutionCommit.lease` and the generation argument
591
+ to `releaseLease`. Initialize `RunLease.generation` to zero for creation or
592
+ acquisition and retain the generation assigned by the store on success.
593
+ Renewal keeps the same generation and cannot revive an expired lease.
594
+
595
+ Renewal errors surface as `FevexRunError` with code `RUN_CONFLICT` and the original
596
+ error in `cause`. Local cleanup does not persist cancellation after ownership is lost.
597
+ A lost, failed or blocked renewal aborts local execution; the runtime will not
598
+ start further work or persist results under that token. The orphaned run remains
599
+ available for recovery. External effects already in flight still need
600
+ idempotency or fencing in the external system.
601
+
602
+ Migrate storage before restarting workers. Legacy leases get generation zero
603
+ and cannot commit or renew; recovery can acquire a new generation after they
604
+ expire. Never run old and new workers together during this upgrade.
605
+
606
+ ## HTTP protocol
607
+
608
+ `@fevex/core/http` exposes the runtime through versioned JSON and SSE using
609
+ native Fetch types:
610
+
611
+ ```ts
612
+ import { createFevexHttpHandler, createFevexHttpClient } from '@fevex/core/http';
613
+
614
+ const handler = createFevexHttpHandler({ fevex: app });
615
+ const client = createFevexHttpClient({ baseUrl: 'http://localhost:3001' });
616
+
617
+ const run = await client.startRun('support', { input: 'Check account 42.' });
618
+ for await (const event of client.observeRun(run.id)) {
619
+ console.log(event.type);
620
+ }
621
+ ```
622
+
623
+ Protocol v3 also exposes `client.startWorkflow(...)` and
624
+ `client.startTeam(...)`; all run kinds share observation, resume, recovery and
625
+ cancellation endpoints.
626
+
627
+ Hosts pass authenticated `ExecutionContext` separately to `handler(request,
628
+ { context })`. Observation reconnects with `Last-Event-ID`; disconnecting an
629
+ SSE response never cancels its run. Cancellation is explicit through
630
+ `client.cancelRun(run.id)`.
631
+
632
+ Artifacts use the same handler:
633
+
634
+ ```ts
635
+ const handler = createFevexHttpHandler({
636
+ fevex: app,
637
+ artifacts: artifactStore, // FileSystemArtifactStore or S3ArtifactStore
638
+ artifactCatalog: artifactCatalog, // optional; enables listing by run
639
+ authorizeArtifact(access) {
640
+ return access.context?.actor?.id === 'alice';
641
+ },
642
+ });
643
+ ```
644
+
645
+ Routes:
646
+
647
+ - `GET /v1/artifacts/:id` streams the bytes with `content-type`,
648
+ `content-length`, `content-disposition`, `etag` and artifact headers.
649
+ - `HEAD /v1/artifacts/:id` returns the same metadata without a body.
650
+ - `DELETE /v1/artifacts/:id` deletes bytes and catalog metadata.
651
+ - `GET /v1/runs/:runId/artifacts?limit=100` lists descriptors for a run.
652
+
653
+ The client exposes `getArtifact`, `listArtifacts` and `deleteArtifact`. Keep the
654
+ bucket private; the handler authorizes the request and streams the bytes, or a
655
+ host can use the descriptor to mint a short-lived signed URL.
656
+
356
657
  ## Approvals and durable runs
357
658
 
358
659
  Tools can declare risk, approval, idempotency, retries and credential names.
@@ -437,6 +738,28 @@ underlying work. External systems must enforce the supplied idempotency key to
437
738
  make keyed retries safe. Repeated tool call IDs are rejected within a run before
438
739
  Fevex repeats an effect.
439
740
 
741
+ ### Deadlines, admission and retention
742
+
743
+ Set `deadlineMs` on a run request to cap wall-clock time. An expired deadline
744
+ cancels the run with `run.cancelled` reason `timeout`; resuming or recovering
745
+ after the absolute deadline persists that cancellation instead of continuing.
746
+
747
+ `createFevex({ admission })` limits concurrent top-level runs globally or per
748
+ tenant. The default tenant key is a non-empty `context.attributes.tenantId`;
749
+ `tenantKey` and `authorize` support host-specific quotas. Counts are local to
750
+ one runtime process.
751
+
752
+ `createFevex({ limits: { maxEventsPerRun } })` caps non-control events per run.
753
+ Pause, resume, approval, elicitation, wait and terminal events are always
754
+ persisted so a run can always reach a durable state; use retention for a total
755
+ storage cap.
756
+
757
+ Terminal runs can be purged with `app.purgeRuns({ before, statuses, limit })`.
758
+ SQLite and PostgreSQL implement the retention capability; custom stores can add
759
+ `purgeRuns` to opt in. The API deletes only terminal runs and sessions left
760
+ without runs; active work is untouched. Artifact retention and per-event payload
761
+ byte limits remain host policies.
762
+
440
763
  ## Observability
441
764
 
442
765
  Add trace exporters to the application configuration; agent definitions stay
@@ -468,8 +791,50 @@ Traces contain only metadata by default. Output and safe error capture require
468
791
  an explicit `content.include` policy; sensitive object keys are redacted before
469
792
  an optional application redactor runs. Exporters run outside the run path: their
470
793
  errors never change the run result, and `flushObservability()` reports pending
471
- export failures. OpenTelemetry SDK setup, OTLP export and shutdown remain
472
- application responsibilities.
794
+ export failures. Export work is bounded and deduplicated:
795
+
796
+ ```ts
797
+ const app = createFevex({
798
+ models,
799
+ agents,
800
+ observability: {
801
+ exporters: [createOpenTelemetryExporter()],
802
+ exportQueue: {
803
+ concurrency: 4,
804
+ maxQueued: 100,
805
+ timeoutMs: 30_000,
806
+ maxFailures: 100,
807
+ },
808
+ },
809
+ });
810
+
811
+ await app.flushObservability({ timeoutMs: 5_000 });
812
+ console.log(app.getObservabilityStats());
813
+ ```
814
+
815
+ Workflow and team traces carry `kind` and a span per step. Agent steps expose
816
+ `childRunId`; child agent traces carry `parentRunId` and `rootRunId`, and tool
817
+ spans are nested under the model call that requested them. A dropped trace or a
818
+ failed export is counted in `getObservabilityStats()`, and the next
819
+ `flushObservability()` rejects with the failures accumulated since the previous
820
+ flush. OpenTelemetry SDK setup, OTLP export and shutdown remain application
821
+ responsibilities.
822
+
823
+ ## Graceful shutdown
824
+
825
+ `shutdown` stops admission, drains active runs, cancels what remains at the
826
+ deadline and flushes trace exports:
827
+
828
+ ```ts
829
+ const result = await app.shutdown({ timeoutMs: 30_000 });
830
+ console.log(result.timedOut, result.cancelledRunIds, result.observability);
831
+ ```
832
+
833
+ New starts, resumes and recoveries fail with `RUNTIME_CLOSED` (HTTP `503`) once
834
+ shutdown begins. The host closes its HTTP server, stores and external adapters
835
+ after `shutdown` returns. See
836
+ [Operations & soak tests](https://fevex.dev/docs/operations/) for crash recovery
837
+ and the 1–8 hour load harness.
473
838
 
474
839
  ## Evaluations
475
840
 
@@ -510,22 +875,361 @@ or `textReporter` with your own write callback, and `compareEvaluationReports`
510
875
  to fail CI when cases disappear, passing scores fail or scores drop beyond the
511
876
  configured tolerance.
512
877
 
878
+ ## Connections
879
+
880
+ Connections turn remote tool providers into normal Fevex tools. Names are local
881
+ and explicit: a connection named `docs` with remote tool `search` is exposed to
882
+ agents as `docs__search`.
883
+
884
+ ```ts
885
+ import { createFevex, defineAgent, defineConnection } from '@fevex/core';
886
+ import { createMcpToolProvider } from '@fevex/mcp';
887
+
888
+ const docs = defineConnection({
889
+ name: 'docs',
890
+ provider: createMcpToolProvider({ url: 'https://example.com/mcp' }),
891
+ allowlist: ['search'],
892
+ timeoutMs: 30_000,
893
+ tools: {
894
+ search: {
895
+ description: 'Search docs.',
896
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
897
+ },
898
+ },
899
+ });
900
+
901
+ createFevex({
902
+ models: {},
903
+ agents: [defineAgent({
904
+ name: 'assistant',
905
+ instructions: 'Help.',
906
+ tools: ['docs__search'],
907
+ })],
908
+ connections: [docs],
909
+ });
910
+ ```
911
+
912
+ Fevex owns allowlists, timeouts, approvals, policies and safe error messages.
913
+ MCP metadata and annotations are treated as untrusted hints. The first MCP
914
+ adapter supports Streamable HTTP only; `stdio`, resources, prompts and legacy
915
+ HTTP+SSE stay out of scope.
916
+
917
+ OpenAPI providers use the same connection contract:
918
+
919
+ ```ts
920
+ import { createOpenApiToolProvider } from '@fevex/openapi';
921
+
922
+ const billing = defineConnection({
923
+ name: 'billing',
924
+ provider: createOpenApiToolProvider({
925
+ document: openApiDocument,
926
+ operations: { allow: ['getInvoice'] },
927
+ headers: () => ({ Authorization: `Bearer ${process.env.BILLING_TOKEN}` }),
928
+ }),
929
+ allowlist: ['getInvoice'],
930
+ });
931
+ ```
932
+
933
+ `@fevex/openapi` accepts bundled JSON OpenAPI 3.1.x documents, local refs and
934
+ JSON request/response bodies. It rejects unsupported JSON Schema keywords,
935
+ remote refs, YAML, OpenAPI 3.0/3.2 and non-JSON payloads instead of silently
936
+ dropping constraints.
937
+
938
+ ## Tool search and large results
939
+
940
+ Agents with large tool catalogs can expose `fevex__search_tools` instead of
941
+ sending every schema on every turn:
942
+
943
+ ```ts
944
+ defineAgent({
945
+ name: 'support',
946
+ instructions: 'Answer with the available tools.',
947
+ tools: [...],
948
+ toolSearch: { mode: 'auto', threshold: 30, pinned: ['accounts_get'] },
949
+ });
950
+ ```
951
+
952
+ `auto` enables search above the threshold, `on` always does and `off` exposes
953
+ every tool. Search only indexes the agent's own tools; allowlists, policies and
954
+ approvals still apply when a loaded tool executes. Add `semantic.embeddings` to
955
+ fuse lexical and vector candidates with Reciprocal Rank Fusion; failures fall
956
+ back to lexical. An optional `semantic.reranker` can reorder the fused top
957
+ candidates.
958
+
959
+ Tools can cap serialized results with `maxResultBytes`, or an agent/run can set
960
+ `limits.maxToolResultBytes`. The default cap is 1 MiB; use `false` to disable it.
961
+ Store larger outputs through `context.artifacts` and return the descriptor;
962
+ otherwise the run fails with `TOOL_RESULT_TOO_LARGE`.
963
+
964
+ ## Channels
965
+
966
+ `@fevex/core/channels` provides a small adapter contract and `handleChannelInput`
967
+ for hosting apps that receive messages from another surface:
968
+
969
+ ```ts
970
+ import { handleChannelInput } from '@fevex/core/channels';
971
+
972
+ const sessions = new Map<string, string>();
973
+
974
+ const result = await handleChannelInput(
975
+ { id: 'm1', conversationId: 'c1', text: 'hello' },
976
+ {
977
+ fevex: app,
978
+ agentName: 'assistant',
979
+ adapter: {
980
+ name: 'memory',
981
+ async parse(input) {
982
+ return {
983
+ id: input.id,
984
+ deliveryId: input.id,
985
+ conversationId: input.conversationId,
986
+ content: input.text,
987
+ };
988
+ },
989
+ async deliver(output) {
990
+ return output;
991
+ },
992
+ },
993
+ resolveSessionId(message) {
994
+ return sessions.get(message.conversationId);
995
+ },
996
+ },
997
+ );
998
+
999
+ if (!result.ignored) sessions.set(result.message.conversationId, result.run.sessionId);
1000
+ ```
1001
+
1002
+ The helper emits local channel events around the run. Slack verification,
1003
+ acknowledgement, durable redelivery and deduplication remain outside this core
1004
+ helper and belong to the Slack adapter phase.
1005
+
1006
+ ## Knowledge and memory
1007
+
1008
+ `@fevex/core/knowledge` keeps external context separate from sessions:
1009
+
1010
+ ```ts
1011
+ import {
1012
+ InMemoryMemoryStore,
1013
+ createFevex,
1014
+ defineAgent,
1015
+ defineContextProvider,
1016
+ defineSkill,
1017
+ } from '@fevex/core';
1018
+
1019
+ const app = createFevex({
1020
+ models,
1021
+ memoryStore: new InMemoryMemoryStore(),
1022
+ contextProviders: [
1023
+ defineSkill({
1024
+ name: 'refunds',
1025
+ instructions: 'Refunds are allowed for 30 days after purchase.',
1026
+ }),
1027
+ defineContextProvider({
1028
+ name: 'account',
1029
+ async read({ context }) {
1030
+ return [{ id: 'tier', content: `Plan: ${context?.attributes?.plan ?? 'free'}` }];
1031
+ },
1032
+ }),
1033
+ ],
1034
+ agents: [
1035
+ defineAgent({
1036
+ name: 'support',
1037
+ instructions: 'Answer with business context.',
1038
+ skills: ['refunds'],
1039
+ context: ['account'],
1040
+ memory: { read: true, write: true },
1041
+ }),
1042
+ ],
1043
+ });
1044
+ ```
1045
+
1046
+ For longer instructions, keep them in an explicit `SKILL.md` file. Only the
1047
+ frontmatter metadata enters the prompt; the body loads on demand:
1048
+
1049
+ ```ts
1050
+ import { createAgentsMdContextProvider, loadSkillFile } from '@fevex/core';
1051
+
1052
+ const refunds = await loadSkillFile('./skills/refunds/SKILL.md');
1053
+
1054
+ const app = createFevex({
1055
+ models,
1056
+ contextProviders: [refunds, createAgentsMdContextProvider('./AGENTS.md')],
1057
+ agents: [
1058
+ defineAgent({
1059
+ name: 'support',
1060
+ instructions: 'Answer with business context.',
1061
+ skills: ['refunds'],
1062
+ context: ['agents-md'],
1063
+ }),
1064
+ ],
1065
+ });
1066
+ ```
1067
+
1068
+ `SKILL.md` uses `name` and `description` frontmatter. The reserved
1069
+ `fevex__load_skill` tool loads the body and lists files under `references/`,
1070
+ `scripts/` and `assets/`; scripts are not executed by the runtime. Paths are
1071
+ explicit, with no automatic discovery.
1072
+
1073
+ Context providers are lazy per run. Memory is a replaceable store; the built-in
1074
+ store is in-memory substring search, not embeddings, vector search or durable
1075
+ storage.
1076
+
1077
+ ## Artifacts
1078
+
1079
+ `@fevex/core/artifacts` stores large or binary outputs as references. Runs,
1080
+ events and prompts carry an `ArtifactDescriptor`; bytes stay in an
1081
+ `ArtifactStore`.
1082
+
1083
+ ```ts
1084
+ import {
1085
+ FileSystemArtifactStore,
1086
+ InMemoryArtifactStore,
1087
+ S3ArtifactStore,
1088
+ type S3ObjectClient,
1089
+ } from '@fevex/core/artifacts';
1090
+
1091
+ const artifacts = new FileSystemArtifactStore({
1092
+ rootDir: '.fevex/artifacts',
1093
+ maxBytes: 10 * 1024 * 1024,
1094
+ });
1095
+
1096
+ const descriptor = await artifacts.put(
1097
+ {
1098
+ filename: 'report.pdf',
1099
+ mimeType: 'application/pdf',
1100
+ bytes,
1101
+ },
1102
+ { tenantId: 'acme', runId: 'run-1' },
1103
+ );
1104
+
1105
+ const stored = await artifacts.get(descriptor.artifactId, { tenantId: 'acme' });
1106
+ ```
1107
+
1108
+ Use `InMemoryArtifactStore` for tests. For S3, R2, MinIO or another
1109
+ S3-compatible service, wrap its SDK with the small `S3ObjectClient` interface;
1110
+ Fevex does not ship an object-storage SDK:
1111
+
1112
+ ```ts
1113
+ const client: S3ObjectClient = {
1114
+ async putObject({ key, body, contentType }) {
1115
+ await s3.send(new PutObjectCommand({
1116
+ Bucket,
1117
+ Key: key,
1118
+ Body: body,
1119
+ ContentType: contentType,
1120
+ }));
1121
+ },
1122
+ async getObject({ key }) {
1123
+ try {
1124
+ const result = await s3.send(new GetObjectCommand({ Bucket, Key: key }));
1125
+ return new Uint8Array(await result.Body!.transformToByteArray());
1126
+ } catch (error) {
1127
+ if ((error as { name?: string }).name === 'NoSuchKey') return null;
1128
+ throw error;
1129
+ }
1130
+ },
1131
+ async deleteObject({ key }) {
1132
+ await s3.send(new DeleteObjectCommand({ Bucket, Key: key }));
1133
+ },
1134
+ };
1135
+
1136
+ const artifacts = new S3ArtifactStore({ client, prefix: 'fevex' });
1137
+ ```
1138
+
1139
+ The bucket should stay private. The host authorizes the actor and serves a
1140
+ short-lived signed URL or streams the bytes; do not expose bucket URLs to the
1141
+ model. This subpath is local-first: use the filesystem store for development and
1142
+ single-node deployments, and the S3-compatible store for multi-instance
1143
+ production.
1144
+
1145
+ For a queryable metadata index, compose the byte store with a catalog:
1146
+
1147
+ ```ts
1148
+ import { CatalogedArtifactStore, FileSystemArtifactStore } from '@fevex/core/artifacts';
1149
+ import { createSQLiteArtifactCatalog } from '@fevex/sqlite';
1150
+
1151
+ const catalog = createSQLiteArtifactCatalog({ filename: '.fevex/artifacts.db' });
1152
+ const artifacts = new CatalogedArtifactStore({
1153
+ store: new FileSystemArtifactStore({ rootDir: '.fevex/artifacts' }),
1154
+ catalog,
1155
+ });
1156
+
1157
+ const records = await artifacts.list({ runId: 'run-1' }, { tenantId: 'acme' });
1158
+ await artifacts.deleteByRun('run-1', { tenantId: 'acme' });
1159
+ ```
1160
+
1161
+ `@fevex/postgres` exposes the same catalog contract with
1162
+ `createPostgresArtifactCatalog`. The catalog stores only metadata; bytes stay in
1163
+ the filesystem or object storage.
1164
+
1165
+ Pass the store to `createFevex` to expose it to tools as `context.artifacts`:
1166
+
1167
+ ```ts
1168
+ const save = defineTool({
1169
+ name: 'save_report',
1170
+ async execute(_input, context) {
1171
+ return context.artifacts!.put({
1172
+ filename: 'report.txt',
1173
+ mimeType: 'text/plain',
1174
+ bytes: new TextEncoder().encode('report'),
1175
+ });
1176
+ },
1177
+ });
1178
+
1179
+ createFevex({
1180
+ models,
1181
+ agents,
1182
+ tools: [save],
1183
+ artifacts,
1184
+ });
1185
+ ```
1186
+
1187
+ The runtime injects `runId`, `sessionId`, tenant and actor into every artifact
1188
+ call. Each successful `put` emits `artifact.created` with the descriptor before
1189
+ `tool.completed`; a tool that writes and then fails still leaves the event and
1190
+ the artifact available.
1191
+
513
1192
  ## Public Exports
514
1193
 
515
1194
  | Import | Purpose |
516
1195
  | --- | --- |
517
1196
  | `@fevex/core` | Common runtime, definitions and contracts |
518
1197
  | `@fevex/core/agents` | Agent definitions |
1198
+ | `@fevex/core/artifacts` | Artifact descriptors, local stores and S3-compatible adapter |
1199
+ | `@fevex/core/channels` | Channel adapters and message handling |
1200
+ | `@fevex/core/knowledge` | Context providers, skills and memory contracts |
519
1201
  | `@fevex/core/models` | Provider-neutral model contracts |
520
- | `@fevex/core/tools` | Tool definitions and execution context |
1202
+ | `@fevex/core/tools` | Tool, connection and provider contracts |
1203
+ | `@fevex/core/workflows` | Durable workflow definitions |
1204
+ | `@fevex/core/teams` | Multiagent team definitions |
521
1205
  | `@fevex/core/runtime` | Runs, sessions and store contracts |
1206
+ | `@fevex/core/sandbox` | Sandbox contract and local development sandbox |
522
1207
  | `@fevex/core/policies` | Authorization policy contracts |
523
- | `@fevex/core/observability` | Trace, redaction and cost contracts |
1208
+ | `@fevex/core/observability` | Trace, bounded export queue, redaction and cost contracts |
524
1209
  | `@fevex/core/evals` | Datasets, scorers, reporters and regressions |
525
1210
  | `@fevex/core/testing` | Deterministic testing helpers |
526
1211
 
527
1212
  All subpaths ship in the same `@fevex/core` package.
528
1213
 
1214
+ ## Source Layout
1215
+
1216
+ Each public subpath is a folder under `src`. Its `index.ts` is intentionally a
1217
+ small export barrel; contracts and implementations live in files named after
1218
+ their responsibility. For example, `runtime/run-store.ts` owns persistence
1219
+ contracts while `runtime/in-memory-run-store.ts` owns the development
1220
+ implementation. Keep provider-neutral public contracts in their domain folder
1221
+ and orchestration details under `src/internal`.
1222
+
1223
+ When adding a feature:
1224
+
1225
+ - extend an existing responsibility before creating a new abstraction;
1226
+ - keep platform or provider-specific behavior outside `@fevex/core`;
1227
+ - export public API only through the domain barrel and preserve existing
1228
+ subpath imports;
1229
+ - document lifecycle, persistence, security and replay semantics; avoid
1230
+ comments that only restate the TypeScript signature;
1231
+ - add the smallest test that exercises the behavior at its public boundary.
1232
+
529
1233
  ## Current Scope
530
1234
 
531
1235
  The MVP intentionally supports:
@@ -543,3 +1247,72 @@ human evaluation workflows.
543
1247
  ## License
544
1248
 
545
1249
  Apache-2.0
1250
+
1251
+ ### Connection discovery isolation
1252
+
1253
+ Connection tool metadata is discovered for each resolution using that caller's
1254
+ context. The core does not cache catalogs or rejected discovery promises across
1255
+ calls. A failed discovery can therefore recover on the next attempt without
1256
+ recreating the runtime. Providers must scope their own state to an identity;
1257
+ `@fevex/mcp` binds each instance to its first resolved header set and rejects
1258
+ changes. Use a separate MCP provider/connection per identity and recreate it when
1259
+ credentials rotate. This adds discovery requests in exchange for avoiding stale
1260
+ or cross-identity catalogs. Tool calls are not automatically replayed by MCP.
1261
+
1262
+ ### Paged event reads and SSE
1263
+
1264
+ `listEvents(runId, { after, limit, order })` returns events whose sequence is
1265
+ strictly greater than the cursor's sequence. `order` is `asc` by default;
1266
+ `{ order: 'desc', limit: 1 }` retrieves the latest event. `limit` must be a positive
1267
+ safe integer. Omitting it preserves the existing complete-result behavior.
1268
+ Unknown cursors and cursors belonging to another run reject with
1269
+ `FevexRunError` code `INVALID_CURSOR` (HTTP 400).
1270
+
1271
+ SQLite and PostgreSQL resolve the cursor by ID and filter, order and limit in
1272
+ SQL using the existing `(run_id, sequence)` index. Prior event payloads are not
1273
+ loaded to find the cursor. Custom run stores must implement these options and
1274
+ the cursor error contract before being used with the updated HTTP handler.
1275
+
1276
+ The HTTP handler accepts `eventPageSize` (default 100, range 1–1000). It uses this
1277
+ limit for the initial SSE read and every subsequent read. Full pages drain
1278
+ without the polling delay; short pages wait for `pollIntervalMs`. Stream demand
1279
+ controls progress, so a slow consumer retains at most one page plus the stream's
1280
+ single queued frame, rather than buffering the run's entire history. Disconnecting
1281
+ cancels polling. `maxConnections` caps concurrent SSE streams. This bounds event
1282
+ counts, not the byte size of an individual event; per-event payload limits remain
1283
+ host responsibilities. Use `purgeRuns` for configurable retention of terminal
1284
+ runs.
1285
+
1286
+ ### Run model identity
1287
+
1288
+ Agent runs retain the effective `modelName` and a `modelMetadata` snapshot when
1289
+ the gateway supplies metadata. Traces and cost calculators use this identity,
1290
+ including per-request model overrides, after terminal checkpoint cleanup and
1291
+ across pause/resume. These fields are optional for older stored runs; resuming
1292
+ an older run fills them from its checkpoint's resolved model.
1293
+
1294
+ ### Local sandbox limits
1295
+
1296
+ The local development sandbox sends `SIGTERM` on timeout or cancellation, then
1297
+ `SIGKILL` after 100 ms if the process has not closed. Cancellation rejects with
1298
+ the caller's reason after process cleanup. Output limits apply separately to
1299
+ stdout and stderr in UTF-8 bytes, omitting an incomplete trailing character.
1300
+ This subprocess control does not provide operating-system isolation.
1301
+
1302
+ ### Production sandbox
1303
+
1304
+ Use `@fevex/sandbox-docker` for isolated runs. It creates one long-lived
1305
+ container and workspace per run, uses `--network none` by default, drops Linux
1306
+ capabilities, applies PID, memory and CPU limits, and only forwards explicit
1307
+ environment variables. Fevex calls `cleanupRun` after terminal runs and
1308
+ `dispose` during shutdown; paused runs keep their resources for resume.
1309
+
1310
+ ```ts
1311
+ import { createDockerSandbox } from '@fevex/sandbox-docker';
1312
+
1313
+ const sandbox = createDockerSandbox({
1314
+ image: 'oven/bun:1-alpine',
1315
+ commands: ['bun', 'sh'],
1316
+ workspaceRoot: '/var/lib/fevex/sandboxes',
1317
+ });
1318
+ ```