@fevex/core 0.1.0-alpha.1 → 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.
- package/README.md +424 -6
- package/dist/agents/index.d.mts +1 -1
- package/dist/agents/index.mjs +1 -1
- package/dist/channels/index.d.mts +2 -0
- package/dist/channels/index.mjs +150 -0
- package/dist/evals/index.d.mts +19 -11
- package/dist/evals/index.mjs +125 -122
- package/dist/fevex-Ddyi6z6L.d.mts +60 -0
- package/dist/http/index.d.mts +90 -0
- package/dist/http/index.mjs +470 -0
- package/dist/{index-ojCY3lwm.d.mts → index-B4PIkqOU.d.mts} +5 -4
- package/dist/index-Bg9rr8Qe.d.mts +50 -0
- package/dist/index-Bgcxgy66.d.mts +82 -0
- package/dist/index-CTIwrVf9.d.mts +51 -0
- package/dist/index-CqOjvtQ9.d.mts +132 -0
- package/dist/index-CrOId2px.d.mts +66 -0
- package/dist/index-DeXUtyd9.d.mts +70 -0
- package/dist/index-DviHtsLo.d.mts +58 -0
- package/dist/index-Dx_LotWZ.d.mts +425 -0
- package/dist/{index-QTkd6a7C.d.mts → index-dkFyTNh4.d.mts} +4 -3
- package/dist/index.d.mts +22 -10
- package/dist/index.mjs +2784 -720
- package/dist/knowledge/index.d.mts +2 -0
- package/dist/knowledge/index.mjs +89 -0
- package/dist/models/index.d.mts +2 -46
- package/dist/observability/index.d.mts +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/policies/index.mjs +1 -1
- package/dist/run-error-Cr17wPGH.mjs +23 -0
- package/dist/runtime/index.d.mts +2 -2
- package/dist/runtime/index.mjs +58 -9
- package/dist/sandbox/index.d.mts +2 -0
- package/dist/sandbox/index.mjs +122 -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 +33 -9
- package/dist/testing/index.mjs +415 -27
- package/dist/tools/index.d.mts +3 -3
- package/dist/tools/index.mjs +222 -2
- package/dist/types-CuoRx12Q.d.mts +333 -0
- package/dist/workflows/index.d.mts +2 -0
- package/dist/workflows/index.mjs +6 -0
- package/package.json +27 -3
- package/dist/fevex-Dz6GBz9o.d.mts +0 -32
- package/dist/index-BzxHfyYV.d.mts +0 -23
- package/dist/index-CZGyhNIQ.d.mts +0 -47
- package/dist/index-C_Zpr13f.d.mts +0 -124
- package/dist/index-DGWOGXzc.d.mts +0 -181
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ For official model adapters:
|
|
|
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/mcp
|
|
37
|
+
npm install @fevex/core @fevex/openapi
|
|
36
38
|
npm install @fevex/core @fevex/sqlite
|
|
37
39
|
npm install @fevex/core @fevex/postgres
|
|
38
40
|
npm install @fevex/core @fevex/opentelemetry @opentelemetry/api
|
|
@@ -106,6 +108,133 @@ console.log(result.output.answer);
|
|
|
106
108
|
`fakeModel` returns its configured responses in order and records every input in
|
|
107
109
|
`model.calls`.
|
|
108
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
|
+
|
|
109
238
|
## Connect A Model
|
|
110
239
|
|
|
111
240
|
Real providers connect through the small `ModelGateway` contract:
|
|
@@ -184,8 +313,26 @@ such as temperature, metadata and provider-only reasoning settings are kept.
|
|
|
184
313
|
|
|
185
314
|
`createFevex` validates the complete composition before returning the app,
|
|
186
315
|
including JavaScript values that bypass TypeScript. Invalid registrations throw
|
|
187
|
-
`FevexConfigurationError` with a stable `code
|
|
188
|
-
|
|
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`.
|
|
189
336
|
|
|
190
337
|
The app captures a shallow snapshot of registered definitions, tool lists,
|
|
191
338
|
limits, model options and `onEvent`. Mutating those caller-owned containers
|
|
@@ -197,13 +344,15 @@ Fevex accepts
|
|
|
197
344
|
[`StandardSchemaV1`](https://github.com/standard-schema/standard-schema)
|
|
198
345
|
validators for:
|
|
199
346
|
|
|
347
|
+
- agent, workflow and team input before it is persisted;
|
|
200
348
|
- tool input before `execute`;
|
|
201
349
|
- tool output before it is returned to the model;
|
|
202
|
-
- 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.
|
|
203
352
|
|
|
204
353
|
Schemas may transform values. The transformed result must still be JSON-safe.
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
207
356
|
accepted by these fields.
|
|
208
357
|
|
|
209
358
|
When a model adapter needs schemas, Fevex converts authoring schemas that also
|
|
@@ -353,6 +502,72 @@ Unknown cursors fail instead of silently replaying events.
|
|
|
353
502
|
it. The default survives only while the process lives, and one run at a time
|
|
354
503
|
may update a session.
|
|
355
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
|
+
|
|
356
571
|
## Approvals and durable runs
|
|
357
572
|
|
|
358
573
|
Tools can declare risk, approval, idempotency, retries and credential names.
|
|
@@ -510,15 +725,166 @@ or `textReporter` with your own write callback, and `compareEvaluationReports`
|
|
|
510
725
|
to fail CI when cases disappear, passing scores fail or scores drop beyond the
|
|
511
726
|
configured tolerance.
|
|
512
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.
|
|
873
|
+
|
|
513
874
|
## Public Exports
|
|
514
875
|
|
|
515
876
|
| Import | Purpose |
|
|
516
877
|
| --- | --- |
|
|
517
878
|
| `@fevex/core` | Common runtime, definitions and contracts |
|
|
518
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 |
|
|
519
882
|
| `@fevex/core/models` | Provider-neutral model contracts |
|
|
520
|
-
| `@fevex/core/tools` | Tool
|
|
883
|
+
| `@fevex/core/tools` | Tool, connection and provider contracts |
|
|
884
|
+
| `@fevex/core/workflows` | Durable workflow definitions |
|
|
885
|
+
| `@fevex/core/teams` | Multiagent team definitions |
|
|
521
886
|
| `@fevex/core/runtime` | Runs, sessions and store contracts |
|
|
887
|
+
| `@fevex/core/sandbox` | Sandbox contract and local development sandbox |
|
|
522
888
|
| `@fevex/core/policies` | Authorization policy contracts |
|
|
523
889
|
| `@fevex/core/observability` | Trace, redaction and cost contracts |
|
|
524
890
|
| `@fevex/core/evals` | Datasets, scorers, reporters and regressions |
|
|
@@ -526,6 +892,25 @@ configured tolerance.
|
|
|
526
892
|
|
|
527
893
|
All subpaths ship in the same `@fevex/core` package.
|
|
528
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.
|
|
913
|
+
|
|
529
914
|
## Current Scope
|
|
530
915
|
|
|
531
916
|
The MVP intentionally supports:
|
|
@@ -543,3 +928,36 @@ human evaluation workflows.
|
|
|
543
928
|
## License
|
|
544
929
|
|
|
545
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.
|
package/dist/agents/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as AgentLimits, r as defineAgent, t as AgentDefinition } from "../index-
|
|
1
|
+
import { n as AgentLimits, r as defineAgent, t as AgentDefinition } from "../index-Dx_LotWZ.mjs";
|
|
2
2
|
export { AgentDefinition, AgentLimits, defineAgent };
|
package/dist/agents/index.mjs
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ChannelEvent, c as ChannelMessage, d as ChannelRunOptions, f as ChannelRunResult, i as ChannelError, l as ChannelOutput, n as ChannelAdapter, o as ChannelEventType, r as ChannelContext, s as ChannelFailurePhase, t as handleChannelInput, u as ChannelRunEvent } from "../index-Bgcxgy66.mjs";
|
|
2
|
+
export { ChannelAdapter, ChannelContext, ChannelError, ChannelEvent, ChannelEventType, ChannelFailurePhase, ChannelMessage, ChannelOutput, ChannelRunEvent, ChannelRunOptions, ChannelRunResult, handleChannelInput };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region src/channels/channel.ts
|
|
2
|
+
var ChannelError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
safeMessage;
|
|
5
|
+
name = "ChannelError";
|
|
6
|
+
constructor(code, safeMessage, options) {
|
|
7
|
+
super(safeMessage, options);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.safeMessage = safeMessage;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/channels/handle-channel-input.ts
|
|
14
|
+
/** Parses one channel input, runs the configured agent, and delivers its output. */
|
|
15
|
+
async function handleChannelInput(input, options) {
|
|
16
|
+
const channelEvents = [];
|
|
17
|
+
const emit = (event) => {
|
|
18
|
+
channelEvents.push(event);
|
|
19
|
+
options.onEvent?.(event);
|
|
20
|
+
};
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = await options.adapter.parse(input, { signal: options.signal });
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
const error = channelError("CHANNEL_PARSE_FAILED", "Channel parse failed", cause);
|
|
26
|
+
emit(failed(options.adapter.name, "parse", error));
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (parsed === null) return {
|
|
30
|
+
ignored: true,
|
|
31
|
+
events: []
|
|
32
|
+
};
|
|
33
|
+
const message = parsed;
|
|
34
|
+
emit(received(options.adapter.name, message));
|
|
35
|
+
let result;
|
|
36
|
+
try {
|
|
37
|
+
const sessionId = await options.resolveSessionId?.(message);
|
|
38
|
+
result = await options.fevex.runAgent(options.agentName, {
|
|
39
|
+
input: message.content,
|
|
40
|
+
...sessionId ? { sessionId } : {},
|
|
41
|
+
context: channelContext(options.adapter.name, message),
|
|
42
|
+
signal: options.signal
|
|
43
|
+
});
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
const error = channelError("CHANNEL_RUN_FAILED", "Channel run failed", cause);
|
|
46
|
+
emit(failed(options.adapter.name, "run", error, message));
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const run = await options.fevex.getRun(result.runId);
|
|
50
|
+
if (!run || run.kind === "workflow" || run.kind === "team") {
|
|
51
|
+
const error = channelError("CHANNEL_RUN_FAILED", "Channel run was not found after completion");
|
|
52
|
+
emit(failed(options.adapter.name, "run", error, message));
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const output = {
|
|
56
|
+
deliveryId: message.deliveryId,
|
|
57
|
+
conversationId: message.conversationId,
|
|
58
|
+
...message.threadId ? { threadId: message.threadId } : {},
|
|
59
|
+
content: stringifyOutput(result.output),
|
|
60
|
+
metadata: {
|
|
61
|
+
runId: result.runId,
|
|
62
|
+
sessionId: result.sessionId
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
let delivery;
|
|
66
|
+
try {
|
|
67
|
+
delivery = await options.adapter.deliver(output, { signal: options.signal });
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
const error = channelError("CHANNEL_DELIVERY_FAILED", "Channel delivery failed", cause);
|
|
70
|
+
emit(failed(options.adapter.name, "deliver", error, message));
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const deliveredEvent = delivered(options.adapter.name, message);
|
|
74
|
+
emit(deliveredEvent);
|
|
75
|
+
return {
|
|
76
|
+
ignored: false,
|
|
77
|
+
message,
|
|
78
|
+
run,
|
|
79
|
+
output,
|
|
80
|
+
delivery,
|
|
81
|
+
events: [
|
|
82
|
+
channelEvents[0],
|
|
83
|
+
...result.events ?? [],
|
|
84
|
+
deliveredEvent
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function channelContext(adapter, message) {
|
|
89
|
+
return {
|
|
90
|
+
namespace: adapter,
|
|
91
|
+
...message.actor ? { actor: message.actor } : {},
|
|
92
|
+
attributes: {
|
|
93
|
+
...message.metadata ?? {},
|
|
94
|
+
channel: adapter,
|
|
95
|
+
messageId: message.id,
|
|
96
|
+
deliveryId: message.deliveryId,
|
|
97
|
+
conversationId: message.conversationId,
|
|
98
|
+
...message.threadId ? { threadId: message.threadId } : {}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function received(adapter, message) {
|
|
103
|
+
return {
|
|
104
|
+
type: "channel.received",
|
|
105
|
+
id: crypto.randomUUID(),
|
|
106
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
107
|
+
adapter,
|
|
108
|
+
messageId: message.id,
|
|
109
|
+
deliveryId: message.deliveryId,
|
|
110
|
+
conversationId: message.conversationId,
|
|
111
|
+
...message.threadId ? { threadId: message.threadId } : {},
|
|
112
|
+
...message.actor ? { actor: message.actor } : {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function delivered(adapter, message) {
|
|
116
|
+
return {
|
|
117
|
+
type: "channel.delivered",
|
|
118
|
+
id: crypto.randomUUID(),
|
|
119
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
120
|
+
adapter,
|
|
121
|
+
messageId: message.id,
|
|
122
|
+
deliveryId: message.deliveryId,
|
|
123
|
+
conversationId: message.conversationId,
|
|
124
|
+
...message.threadId ? { threadId: message.threadId } : {}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function failed(adapter, phase, error, message) {
|
|
128
|
+
return {
|
|
129
|
+
type: "channel.failed",
|
|
130
|
+
id: crypto.randomUUID(),
|
|
131
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
132
|
+
adapter,
|
|
133
|
+
phase,
|
|
134
|
+
error: error.safeMessage,
|
|
135
|
+
...message ? {
|
|
136
|
+
messageId: message.id,
|
|
137
|
+
deliveryId: message.deliveryId,
|
|
138
|
+
conversationId: message.conversationId,
|
|
139
|
+
...message.threadId ? { threadId: message.threadId } : {}
|
|
140
|
+
} : {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function channelError(code, safeMessage, cause) {
|
|
144
|
+
return new ChannelError(code, safeMessage, cause === void 0 ? void 0 : { cause });
|
|
145
|
+
}
|
|
146
|
+
function stringifyOutput(value) {
|
|
147
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { ChannelError, handleChannelInput };
|