@xandreed/sdk 0.12.0 → 0.13.0
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 +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/package.json +8 -10
- package/dist/approval.adapter.d.ts +0 -5
- package/dist/approval.adapter.js +0 -11
- package/dist/domain-session.adapter.d.ts +0 -27
- package/dist/domain-session.adapter.js +0 -56
- package/dist/domain-session.adapter.test.d.ts +0 -1
- package/dist/harness.d.ts +0 -43
- package/dist/harness.js +0 -186
- package/dist/harness.test.d.ts +0 -1
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
export * from "@xandreed/core";
|
|
2
|
-
export * from "./harness.js";
|
|
3
|
-
export * from "./approval.adapter.js";
|
|
4
|
-
export { domainLoop, domainSession } from "./domain-session.adapter.js";
|
|
5
2
|
export { Agent } from "./agent.adapter.js";
|
|
6
3
|
export type { AgentConfig, AgentPluginEntry } from "./agent.adapter.js";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=24",
|
|
6
|
+
"bun": ">=1.3.0"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"main": "./dist/index.js",
|
|
6
10
|
"exports": {
|
|
@@ -10,11 +14,9 @@
|
|
|
10
14
|
}
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"@xandreed/core": "0.
|
|
14
|
-
"effect": "4.0.
|
|
15
|
-
"@xandreed/runtime": "0.
|
|
16
|
-
"@xandreed/plugin-memory": "0.12.0",
|
|
17
|
-
"@xandreed/plugin-session-sqlite": "0.12.0"
|
|
17
|
+
"@xandreed/core": "0.13.0",
|
|
18
|
+
"effect": "4.0.7",
|
|
19
|
+
"@xandreed/runtime": "0.13.0"
|
|
18
20
|
},
|
|
19
21
|
"private": false,
|
|
20
22
|
"types": "./dist/index.d.ts",
|
|
@@ -25,9 +27,5 @@
|
|
|
25
27
|
"README.md",
|
|
26
28
|
"LICENSE"
|
|
27
29
|
],
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=24",
|
|
30
|
-
"bun": ">=1.3.0"
|
|
31
|
-
},
|
|
32
30
|
"license": "MIT"
|
|
33
31
|
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { Approval } from "@xandreed/core";
|
|
3
|
-
import type { HarnessError } from "@xandreed/core";
|
|
4
|
-
/** A host supplies interaction; unattended hosts deny requests by default. */
|
|
5
|
-
export declare const approvalPlugin: (request?: (description: string) => Effect.Effect<boolean, HarnessError>) => import("@xandreed/core").TypedPlugin<{}, {}, Approval, never, never>;
|
package/dist/approval.adapter.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer, Schema } from "effect";
|
|
2
|
-
import { Approval, definePlugin } from "@xandreed/core";
|
|
3
|
-
export const approvalPlugin = (request = () => Effect.succeed(!1)) => definePlugin({
|
|
4
|
-
id: "efferent/approval-host",
|
|
5
|
-
version: "0.12.0",
|
|
6
|
-
scope: "runtime",
|
|
7
|
-
config: Schema.Struct({}),
|
|
8
|
-
defaults: {},
|
|
9
|
-
provides: [Approval],
|
|
10
|
-
layer: () => Layer.succeed(Approval, { request })
|
|
11
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Effect, Option } from "effect";
|
|
2
|
-
import { HarnessError, SessionStore } from "@xandreed/core";
|
|
3
|
-
import type { ConversationId, Session, SessionHandle } from "@xandreed/core";
|
|
4
|
-
/**
|
|
5
|
-
* Adapt a domain session's event protocol to the durable harness lifecycle.
|
|
6
|
-
* When the host provides a ConversationStore, the domain session's writes to
|
|
7
|
-
* its conversation are bound to the harness run (see `turnBound`). They are
|
|
8
|
-
* recorded beside the turn, not through its writer.
|
|
9
|
-
*/
|
|
10
|
-
export declare const domainLoop: <E extends {
|
|
11
|
-
readonly type: string;
|
|
12
|
-
}, R>(options: {
|
|
13
|
-
readonly create: (id: ConversationId) => Effect.Effect<Session<E>, never, R>;
|
|
14
|
-
readonly snapshot?: (id: ConversationId) => Effect.Effect<Readonly<Record<string, unknown>>, HarnessError, R>;
|
|
15
|
-
readonly restore?: (id: ConversationId, data: Readonly<Record<string, unknown>>) => Effect.Effect<void, HarnessError, R>;
|
|
16
|
-
readonly result: (event: E) => Option.Option<{
|
|
17
|
-
readonly text: string;
|
|
18
|
-
readonly outcome: "completed" | "partial";
|
|
19
|
-
}>;
|
|
20
|
-
}) => Effect.Effect<{
|
|
21
|
-
readonly run: (input: import("@xandreed/core").LoopInput) => Effect.Effect<{
|
|
22
|
-
readonly text: string;
|
|
23
|
-
readonly outcome: "completed" | "partial";
|
|
24
|
-
}, HarnessError>;
|
|
25
|
-
}, HarnessError, import("effect/Scope").Scope | SessionStore | R>;
|
|
26
|
-
/** Preserve a domain host's public stream vocabulary while the SDK owns lifecycle. */
|
|
27
|
-
export declare const domainSession: <E>(handle: SessionHandle, decode: (value: unknown) => Option.Option<E>, onError: (message: string) => E) => Session<E>;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Fiber, Filter, Option, Ref, Stream } from "effect";
|
|
2
|
-
import { AgentLoop, ConversationStore, HarnessError, SessionEnvironment, SessionStore, StoreError } from "@xandreed/core";
|
|
3
|
-
const turnBound = (store, id, active, current) => {
|
|
4
|
-
const guard = (target, write) => target !== id ? write : Effect.gen(function* () {
|
|
5
|
-
const origin = yield* current, running = yield* Ref.get(active);
|
|
6
|
-
return yield* Option.isSome(origin) && Option.contains(running, origin.value) ? write : Effect.fail(new StoreError({ message: `conversation ${id}: its turn is over, so the write is refused` }));
|
|
7
|
-
});
|
|
8
|
-
return ConversationStore.of({
|
|
9
|
-
...store,
|
|
10
|
-
append: (target, message) => guard(target, store.append(target, message)),
|
|
11
|
-
appendAll: (target, messages) => guard(target, store.appendAll(target, messages)),
|
|
12
|
-
checkpoint: (target, summary) => guard(target, store.checkpoint(target, summary)),
|
|
13
|
-
checkpointAt: (target, summary, position) => guard(target, store.checkpointAt(target, summary, position)),
|
|
14
|
-
setTitle: (target, title) => guard(target, store.setTitle(target, title)),
|
|
15
|
-
recordOutcome: (target, outcome, reason) => guard(target, store.recordOutcome(target, outcome, reason))
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
export const domainLoop = (options) => Effect.gen(function* () {
|
|
19
|
-
const context = yield* Effect.context(), store = yield* SessionStore, environment = yield* Effect.serviceOption(SessionEnvironment), conversations = yield* Effect.serviceOption(ConversationStore), currentDomainRun = Context.Reference(`@xandreed/sdk/domainRun/${yield* Effect.sync(() => crypto.randomUUID())}`, { defaultValue: () => Option.none() }), running = yield* Ref.make(Option.none()), acquire = (id) => Effect.gen(function* () {
|
|
20
|
-
if (options.restore !== void 0) {
|
|
21
|
-
const snapshot = (yield* store.read(id, -1)).filter((event) => event.name === "domain.snapshot").at(-1);
|
|
22
|
-
if (snapshot !== void 0)
|
|
23
|
-
yield* options.restore(id, snapshot.data).pipe(Effect.provide(context));
|
|
24
|
-
}
|
|
25
|
-
const created = options.create(id);
|
|
26
|
-
return yield* Option.match(conversations, {
|
|
27
|
-
onNone: () => created,
|
|
28
|
-
onSome: (conversation) => created.pipe(Effect.provideService(ConversationStore, turnBound(conversation, id, running, currentDomainRun)))
|
|
29
|
-
}).pipe(Effect.provide(context));
|
|
30
|
-
}), record = Option.isSome(environment) ? environment.value.session : void 0, active = yield* Ref.make(record === void 0 ? Option.none() : Option.some(yield* acquire(record.id)));
|
|
31
|
-
yield* Effect.addFinalizer(() => Ref.get(active).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (session) => session.shutdown }))));
|
|
32
|
-
return AgentLoop.of({ run: (input) => Effect.scoped(Effect.gen(function* () {
|
|
33
|
-
const token = yield* Effect.sync(() => crypto.randomUUID());
|
|
34
|
-
yield* Ref.set(running, Option.some(token));
|
|
35
|
-
const found = yield* Ref.get(active), session = Option.isSome(found) ? found.value : yield* acquire(input.session.id).pipe(Effect.tap((value) => Ref.set(active, Option.some(value)))), cursor = (yield* session.state).cursor, result = yield* Ref.make(Option.none()), collector = yield* Effect.forkScoped(session.subscribe(cursor).pipe(Stream.tap(({ event }) => input.publish({ name: "domain.event", runId: input.runId, data: { event } })), Stream.tap(({ event }) => Ref.update(result, (current) => Option.orElse(options.result(event), () => current))), Stream.takeUntil(({ event }) => Option.isSome(options.result(event))), Stream.runDrain));
|
|
36
|
-
yield* Effect.forkScoped(session.transient.pipe(Stream.runForEach((event) => input.transient({ name: "domain.delta", runId: input.runId, data: { event } }))));
|
|
37
|
-
yield* session.send(input.userMessage.text).pipe(Effect.provideService(currentDomainRun, Option.some(token)));
|
|
38
|
-
if (!(yield* session.state).log.filter((entry) => entry.seq >= cursor).some(({ event }) => Option.isSome(options.result(event))))
|
|
39
|
-
return yield* Effect.fail(new HarnessError({ code: "domain.unsettled", message: "The domain turn ended without a terminal event" }));
|
|
40
|
-
yield* Fiber.join(collector);
|
|
41
|
-
if (options.snapshot !== void 0)
|
|
42
|
-
yield* options.snapshot(input.session.id).pipe(Effect.provide(context), Effect.flatMap((data) => input.publish({ name: "domain.snapshot", runId: input.runId, data })));
|
|
43
|
-
return yield* Ref.get(result).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.fail(new HarnessError({ code: "domain.result", message: "The domain session ended without a result" })), onSome: Effect.succeed })));
|
|
44
|
-
})).pipe(Effect.ensuring(Ref.set(running, Option.none())), Effect.onInterrupt(() => Ref.get(active).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (session) => session.interrupt }))))) });
|
|
45
|
-
}), domainSession = (handle, decode, onError) => {
|
|
46
|
-
const event = (name, data) => name === "domain.event" ? decode(data.event) : ["run.failed", "run.cancelled"].includes(name) ? Option.some(onError(String(data.message))) : Option.none();
|
|
47
|
-
return {
|
|
48
|
-
conversationId: handle.record.id,
|
|
49
|
-
send: (text) => handle.send(text).pipe(Effect.ignore),
|
|
50
|
-
interrupt: handle.interrupt,
|
|
51
|
-
shutdown: handle.close,
|
|
52
|
-
state: handle.history.pipe(Effect.map((events) => ({ cursor: (events.at(-1)?.seq ?? -1) + 1, log: events.flatMap((entry) => Option.toArray(Option.map(event(entry.name, entry.data), (value) => ({ seq: entry.seq, event: value })))) })), Effect.orDie),
|
|
53
|
-
subscribe: (since) => handle.events(since - 1).pipe(Stream.map((entry) => Option.map(event(entry.name, entry.data), (value) => ({ seq: entry.seq, event: value }))), Stream.filterMap(Filter.fromPredicateOption((value) => value)), Stream.orDie),
|
|
54
|
-
transient: handle.transient.pipe(Stream.filterMap(Filter.fromPredicateOption((entry) => entry.name === "domain.delta" ? decode(entry.data.event) : Option.none())))
|
|
55
|
-
};
|
|
56
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/harness.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Effect, Scope } from "effect";
|
|
2
|
-
import { HarnessError } from "@xandreed/core";
|
|
3
|
-
import type { ConversationId, HarnessConfig, Plugin, SessionHandle } from "@xandreed/core";
|
|
4
|
-
import type { PluginGraph } from "@xandreed/runtime";
|
|
5
|
-
export declare const makeHarness: (options: {
|
|
6
|
-
readonly config: HarnessConfig;
|
|
7
|
-
readonly plugins: ReadonlyArray<Plugin>;
|
|
8
|
-
readonly workspace: string;
|
|
9
|
-
}) => Effect.Effect<{
|
|
10
|
-
create: () => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
11
|
-
resume: (id: ConversationId) => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
12
|
-
fork: (id: ConversationId, through: number) => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
13
|
-
list: Effect.Effect<readonly {
|
|
14
|
-
readonly createdAt: number;
|
|
15
|
-
readonly id: string & import("effect/Brand").Brand<"ConversationId">;
|
|
16
|
-
readonly profile: string;
|
|
17
|
-
readonly workspace: string;
|
|
18
|
-
readonly parent?: (string & import("effect/Brand").Brand<"ConversationId">) | undefined;
|
|
19
|
-
}[], HarnessError, never>;
|
|
20
|
-
graph: Effect.Effect<PluginGraph, never, never>;
|
|
21
|
-
reconfigure: (config: HarnessConfig, plugins?: ReadonlyArray<Plugin>) => Effect.Effect<"applied" | "restart-required", HarnessError, never>;
|
|
22
|
-
}, HarnessError, Scope.Scope>;
|
|
23
|
-
export declare const Harness: {
|
|
24
|
-
make: (options: {
|
|
25
|
-
readonly config: HarnessConfig;
|
|
26
|
-
readonly plugins: ReadonlyArray<Plugin>;
|
|
27
|
-
readonly workspace: string;
|
|
28
|
-
}) => Effect.Effect<{
|
|
29
|
-
create: () => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
30
|
-
resume: (id: ConversationId) => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
31
|
-
fork: (id: ConversationId, through: number) => Effect.Effect<SessionHandle, HarnessError, never>;
|
|
32
|
-
list: Effect.Effect<readonly {
|
|
33
|
-
readonly createdAt: number;
|
|
34
|
-
readonly id: string & import("effect/Brand").Brand<"ConversationId">;
|
|
35
|
-
readonly profile: string;
|
|
36
|
-
readonly workspace: string;
|
|
37
|
-
readonly parent?: (string & import("effect/Brand").Brand<"ConversationId">) | undefined;
|
|
38
|
-
}[], HarnessError, never>;
|
|
39
|
-
graph: Effect.Effect<PluginGraph, never, never>;
|
|
40
|
-
reconfigure: (config: HarnessConfig, plugins?: ReadonlyArray<Plugin>) => Effect.Effect<"applied" | "restart-required", HarnessError, never>;
|
|
41
|
-
}, HarnessError, Scope.Scope>;
|
|
42
|
-
};
|
|
43
|
-
export type Harness = Effect.Success<ReturnType<typeof makeHarness>>;
|
package/dist/harness.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { Cause, Clock, Context, Effect, Exit, Fiber, Option, PubSub, Ref, Scope, Stream, Semaphore } from "effect";
|
|
2
|
-
import { AgentLoop, HarnessError, SessionEnvironment, SessionStore, Sessions, TurnHooks, UserMessage } from "@xandreed/core";
|
|
3
|
-
import { activateGraph, graphFingerprint, resolveGraph } from "@xandreed/runtime";
|
|
4
|
-
const failure = (code, message) => new HarnessError({ code, message }), pendingAfter = (queue, events) => events.reduce((pending, event) => {
|
|
5
|
-
if (event.name === "input.queued" && typeof event.data.id === "string" && typeof event.data.text === "string") {
|
|
6
|
-
const id = event.data.id;
|
|
7
|
-
return pending.some((input) => input.id === id) ? pending : [...pending, { id, text: event.data.text, kind: event.data.kind === "steer" ? "steer" : "turn" }];
|
|
8
|
-
}
|
|
9
|
-
return event.name === "input.claimed" ? pending.filter((input) => input.id !== event.data.id) : pending;
|
|
10
|
-
}, queue), advance = (state, events) => {
|
|
11
|
-
const fresh = events.filter((event) => event.seq > state.seen);
|
|
12
|
-
return fresh.length === 0 ? state : { seen: fresh.reduce((last, event) => Math.max(last, event.seq), state.seen), queue: pendingAfter(state.queue, fresh) };
|
|
13
|
-
}, isClosed = (error) => error.code === "turn.closed", service = (context, tag) => Option.match(Context.getOption(context, tag), {
|
|
14
|
-
onNone: () => Effect.fail(failure("service.missing", `The profile does not provide ${tag.key}`)),
|
|
15
|
-
onSome: Effect.succeed
|
|
16
|
-
});
|
|
17
|
-
export const makeHarness = (options) => Effect.gen(function* () {
|
|
18
|
-
const parent = yield* Effect.scope, initial = yield* resolveGraph(options.config, options.plugins, [SessionEnvironment.key]), graphRef = yield* Ref.make(initial), registryRef = yield* Ref.make(options.plugins), seed = Context.make(SessionEnvironment, { workspace: options.workspace }), runtime = yield* activateGraph(initial, "runtime", Context.makeUnsafe(seed.mapUnsafe), parent), store = yield* service(runtime, SessionStore), sessions = yield* Option.match(Context.getOption(runtime, Sessions), {
|
|
19
|
-
onNone: () => Effect.fail(failure("service.missing", `Harness requires ${Sessions.key}: add sessionsPlugin (@xandreed/plugin-sessions) to the configuration beside the session storage plugin`)),
|
|
20
|
-
onSome: Effect.succeed
|
|
21
|
-
}), handles = yield* Ref.make(new Map), open = (record) => Effect.gen(function* () {
|
|
22
|
-
const existing = (yield* Ref.get(handles)).get(record.id);
|
|
23
|
-
if (existing !== void 0)
|
|
24
|
-
return existing;
|
|
25
|
-
const journal = yield* store.read(record.id, -1), durable = yield* sessions.get({ id: record.id, owner: options.workspace }).pipe(Effect.mapError((error) => failure("session.log", String(error)))), abandoned = journal.reduce((runs, event) => {
|
|
26
|
-
if (event.runId === void 0)
|
|
27
|
-
return runs;
|
|
28
|
-
return event.name === "run.started" ? [...runs, event.runId] : ["run.completed", "run.cancelled", "run.failed"].includes(event.name) ? runs.filter((id) => id !== event.runId) : runs;
|
|
29
|
-
}, []);
|
|
30
|
-
yield* Effect.forEach(abandoned.filter((runId) => !Option.exists(durable.open, (open) => open.runId === runId)), (runId) => store.append(record.id, { name: "run.cancelled", runId, data: { reason: "process interrupted before settlement; tool effects may be incomplete" } }));
|
|
31
|
-
const pending = yield* Ref.make(advance({ seen: -1, queue: [] }, journal)), notifications = yield* PubSub.sliding(1), transientHub = yield* PubSub.sliding(128), running = yield* Ref.make(Option.none()), busy = yield* Ref.make(!1), closed = yield* Ref.make(!1), writing = yield* Ref.make(Option.none()), position = yield* Ref.make(-1), publishing = yield* Semaphore.make(1), gate = yield* Semaphore.make(1), acquire = (graph) => Effect.gen(function* () {
|
|
32
|
-
const scope = yield* Scope.make(), sessionSeed = Context.add(runtime, SessionEnvironment, { workspace: options.workspace, session: record }), context = yield* activateGraph(graph, "session", Context.makeUnsafe(sessionSeed.mapUnsafe), scope).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
|
|
33
|
-
yield* service(context, AgentLoop).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
|
|
34
|
-
return { context, scope, graph, fingerprint: graphFingerprint(graph) };
|
|
35
|
-
}), mounted = yield* Ref.make(yield* acquire(yield* Ref.get(graphRef))), publishUnlocked = (body, outside = !1) => Ref.get(writing).pipe(Effect.flatMap(Option.match({
|
|
36
|
-
onNone: () => store.append(record.id, body),
|
|
37
|
-
onSome: (writer) => writer.transact((foreign) => Effect.gen(function* () {
|
|
38
|
-
const foreignPosition = foreign.reduce((max, entry) => entry.kind === "harness.event" && typeof entry.data.event === "object" && entry.data.event !== null && ("seq" in entry.data.event) && typeof entry.data.event.seq === "number" ? Math.max(max, entry.data.event.seq) : max, -1), seq = Math.max(yield* Ref.get(position), foreignPosition) + 1, event = { ...body, version: 1, id: yield* Effect.sync(() => crypto.randomUUID()), sessionId: record.id, seq, at: yield* Clock.currentTimeMillis };
|
|
39
|
-
return { drafts: [{ kind: "harness.event", data: { event } }], result: event };
|
|
40
|
-
})).pipe(Effect.map((done) => done.result), Effect.tap((event) => Ref.set(position, event.seq)), Effect.catchTag("HarnessError", (error) => outside && isClosed(error) ? store.append(record.id, body) : Effect.fail(error)))
|
|
41
|
-
})), Effect.tap(() => PubSub.publish(notifications, void 0))), publish = (body) => publishing.withPermits(1)(publishUnlocked(body)), refreshPending = Ref.get(pending).pipe(Effect.flatMap((state) => store.read(record.id, state.seen)), Effect.flatMap((events) => Ref.update(pending, (state) => advance(state, events)))), assertOpen = Ref.get(closed).pipe(Effect.flatMap((value) => value ? Effect.fail(failure("session.closed", "Session is closed")) : Effect.void)), refresh = Effect.gen(function* () {
|
|
42
|
-
if (yield* Ref.get(busy))
|
|
43
|
-
return;
|
|
44
|
-
const graph = yield* Ref.get(graphRef), previous = yield* Ref.get(mounted);
|
|
45
|
-
if (graphFingerprint(graph) === previous.fingerprint)
|
|
46
|
-
return;
|
|
47
|
-
const next = yield* acquire(graph);
|
|
48
|
-
yield* Ref.set(mounted, next);
|
|
49
|
-
yield* Scope.close(previous.scope, Exit.void);
|
|
50
|
-
yield* publish({ name: "config.applied", data: { fingerprint: next.fingerprint } });
|
|
51
|
-
}), endedElsewhere = (writer) => sessions.read({ id: record.id, owner: options.workspace }, { after: writer.started.seq, kinds: ["turn.ended"] }).pipe(Effect.map((events) => Option.map(Option.fromNullishOr(events.find((event) => Option.contains(event.turn, writer.admitted.turn))), (event) => event.data.reason === "cancelled" ? "cancelled" : "interrupted")), Effect.catchTag("SessionMissing", () => Effect.succeed(Option.some("removed"))), Effect.mapError((error) => failure("session.log", error.message))), claim = (input) => publish({ name: "input.claimed", data: { id: input.id } }).pipe(Effect.andThen(refreshPending)), steering = Effect.gen(function* () {
|
|
52
|
-
yield* refreshPending;
|
|
53
|
-
const inputs = (yield* Ref.get(pending)).queue.filter((input) => input.kind === "steer");
|
|
54
|
-
yield* Effect.forEach(inputs, claim);
|
|
55
|
-
return inputs.length === 0 ? Option.none() : Option.some(inputs.map((input) => input.text).join(`
|
|
56
|
-
|
|
57
|
-
`));
|
|
58
|
-
}), run = (input) => Effect.scoped(Effect.gen(function* () {
|
|
59
|
-
yield* refresh;
|
|
60
|
-
const current = yield* Ref.get(mounted), loop = yield* service(current.context, AgentLoop), hooks = Context.getOption(current.context, TurnHooks), runId = crypto.randomUUID(), begun = yield* Effect.result(sessions.begin({ id: record.id, owner: options.workspace }, {
|
|
61
|
-
_tag: "User",
|
|
62
|
-
userMessage: new UserMessage({ text: input.text }),
|
|
63
|
-
runId,
|
|
64
|
-
key: input.id,
|
|
65
|
-
command: { kind: input.kind }
|
|
66
|
-
}));
|
|
67
|
-
if (begun._tag === "Failure") {
|
|
68
|
-
if (begun.failure._tag === "TurnDuplicate") {
|
|
69
|
-
yield* claim(input);
|
|
70
|
-
return !0;
|
|
71
|
-
}
|
|
72
|
-
return yield* Effect.fail(failure(begun.failure._tag === "SessionBusy" ? "session.busy" : "session.begin", `The turn could not begin (${begun.failure._tag})`));
|
|
73
|
-
}
|
|
74
|
-
const writer = begun.success;
|
|
75
|
-
yield* refreshPending;
|
|
76
|
-
if (!(yield* Ref.get(pending)).queue.some((item) => item.id === input.id)) {
|
|
77
|
-
yield* writer.end({ reason: "cancelled", failure: Option.some({ code: "input.consumed", message: "the input was consumed by an earlier turn" }) }).pipe(Effect.catchTag("HarnessError", (error) => isClosed(error) ? Effect.void : Effect.fail(error)));
|
|
78
|
-
return !0;
|
|
79
|
-
}
|
|
80
|
-
yield* Ref.set(position, (yield* Ref.get(pending)).seen);
|
|
81
|
-
yield* Ref.set(writing, Option.some(writer));
|
|
82
|
-
yield* Effect.addFinalizer(() => Ref.set(writing, Option.none()));
|
|
83
|
-
yield* claim(input);
|
|
84
|
-
const started = yield* publish({ name: "run.started", runId, data: { config: current.fingerprint } });
|
|
85
|
-
if (!(yield* store.read(record.id, started.seq - 1)).some((event) => event.id === started.id))
|
|
86
|
-
return yield* Effect.fail(failure("session.store", "The SessionStore does not read the events Sessions writes: it must be the projection over the same SessionLog"));
|
|
87
|
-
const args = {
|
|
88
|
-
session: record,
|
|
89
|
-
runId,
|
|
90
|
-
userMessage: new UserMessage({ text: input.text }),
|
|
91
|
-
system: current.graph.config.system ?? "You are a helpful agent.",
|
|
92
|
-
publish,
|
|
93
|
-
transient: (event) => PubSub.publish(transientHub, event).pipe(Effect.asVoid),
|
|
94
|
-
steering,
|
|
95
|
-
history: (after, names) => store.read(record.id, after).pipe(Effect.map((events) => names.length === 0 ? events : events.filter((event) => names.includes(event.name)))),
|
|
96
|
-
services: Context.empty()
|
|
97
|
-
}, task = Effect.gen(function* () {
|
|
98
|
-
const prepared = Option.isSome(hooks) ? yield* hooks.value.before(args) : args, result = yield* loop.run(prepared);
|
|
99
|
-
if (Option.isSome(hooks))
|
|
100
|
-
yield* hooks.value.after(prepared);
|
|
101
|
-
return result;
|
|
102
|
-
}).pipe(Effect.interruptible, Effect.raceFirst(writer.closed.pipe(Effect.flatMap((ended) => Effect.fail(failure("turn.closed", `The turn was ${ended.reason}`))))));
|
|
103
|
-
yield* Ref.set(busy, !0);
|
|
104
|
-
yield* Effect.addFinalizer(() => Ref.set(busy, !1));
|
|
105
|
-
const fiber = yield* Effect.forkIn(task, parent);
|
|
106
|
-
yield* Ref.set(running, Option.some(fiber));
|
|
107
|
-
const exit = yield* Fiber.await(fiber);
|
|
108
|
-
yield* Ref.set(running, Option.none());
|
|
109
|
-
const elsewhere = yield* endedElsewhere(writer), completed = Exit.isSuccess(exit) && Option.isNone(elsewhere), cancelled = Option.isSome(elsewhere) || Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause), message = Option.match(elsewhere, {
|
|
110
|
-
onNone: () => Exit.isFailure(exit) ? Cause.hasInterruptsOnly(exit.cause) ? "Cancelled; unfinished tool effects are not replayed" : Cause.pretty(exit.cause) : "",
|
|
111
|
-
onSome: (reason) => `The turn was ${reason} elsewhere; unfinished tool effects are not replayed`
|
|
112
|
-
});
|
|
113
|
-
if (!Option.contains(elsewhere, "removed"))
|
|
114
|
-
yield* publishing.withPermits(1)(publishUnlocked(completed && Exit.isSuccess(exit) ? { name: "run.completed", runId, data: { ...exit.value } } : { name: cancelled ? "run.cancelled" : "run.failed", runId, data: { message } }, !0));
|
|
115
|
-
yield* writer.end({
|
|
116
|
-
reason: completed && Exit.isSuccess(exit) ? exit.value.outcome : cancelled ? "cancelled" : "failed",
|
|
117
|
-
failure: completed ? Option.none() : Option.some({ code: "run.failed", message })
|
|
118
|
-
}).pipe(Effect.catchTag("HarnessError", (error) => isClosed(error) ? Effect.void : Effect.fail(error)));
|
|
119
|
-
if (Option.isSome(elsewhere) && elsewhere.value !== "cancelled")
|
|
120
|
-
return yield* Effect.fail(failure("turn.closed", `The turn was ${elsewhere.value} elsewhere`));
|
|
121
|
-
if (Exit.isFailure(exit) && !cancelled)
|
|
122
|
-
return yield* Effect.fail(failure("run.failed", Cause.pretty(exit.cause)));
|
|
123
|
-
return completed;
|
|
124
|
-
})).pipe(Effect.uninterruptible), runPending = () => Effect.gen(function* () {
|
|
125
|
-
if (yield* Ref.get(closed))
|
|
126
|
-
return;
|
|
127
|
-
yield* refreshPending;
|
|
128
|
-
const next = (yield* Ref.get(pending)).queue[0];
|
|
129
|
-
if (next !== void 0 && (yield* run(next)))
|
|
130
|
-
yield* runPending();
|
|
131
|
-
}), drain = gate.withPermits(1)(assertOpen.pipe(Effect.andThen(runPending()))), enqueue = (text, kind) => Effect.gen(function* () {
|
|
132
|
-
yield* assertOpen;
|
|
133
|
-
if (text.trim().length === 0)
|
|
134
|
-
return yield* Effect.fail(failure("input.empty", "Enter a message"));
|
|
135
|
-
const input = { id: crypto.randomUUID(), text, kind };
|
|
136
|
-
yield* publishing.withPermits(1)(publishUnlocked({ name: "input.queued", data: input }, !0));
|
|
137
|
-
yield* refreshPending;
|
|
138
|
-
}), interrupt = Ref.get(running).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (fiber) => Fiber.interrupt(fiber).pipe(Effect.asVoid) }))), close = Ref.getAndSet(closed, !0).pipe(Effect.flatMap((already) => already ? Effect.void : Effect.gen(function* () {
|
|
139
|
-
yield* interrupt;
|
|
140
|
-
yield* gate.withPermits(1)(Ref.get(mounted).pipe(Effect.flatMap((current) => Scope.close(current.scope, Exit.void))));
|
|
141
|
-
yield* PubSub.shutdown(notifications);
|
|
142
|
-
yield* PubSub.shutdown(transientHub);
|
|
143
|
-
yield* Ref.update(handles, (all) => new Map([...all].filter(([id]) => id !== record.id)));
|
|
144
|
-
}))), handle = {
|
|
145
|
-
record,
|
|
146
|
-
use: (tag, run) => gate.withPermits(1)(assertOpen.pipe(Effect.andThen(Ref.get(mounted)), Effect.flatMap((current) => service(current.context, tag)), Effect.flatMap(run))),
|
|
147
|
-
send: (text) => enqueue(text, "turn").pipe(Effect.andThen(drain)),
|
|
148
|
-
steer: (text) => enqueue(text, "steer"),
|
|
149
|
-
continue: drain,
|
|
150
|
-
interrupt,
|
|
151
|
-
close,
|
|
152
|
-
busy: Ref.get(busy),
|
|
153
|
-
pending: Ref.get(pending).pipe(Effect.map((state) => state.queue)),
|
|
154
|
-
refresh: gate.withPermits(1)(refresh),
|
|
155
|
-
history: store.read(record.id, -1),
|
|
156
|
-
transient: Stream.fromPubSub(transientHub),
|
|
157
|
-
events: (after = -1) => Stream.unwrap(Effect.gen(function* () {
|
|
158
|
-
const subscription = yield* PubSub.subscribe(notifications), cursor = yield* Ref.make(after);
|
|
159
|
-
return Stream.concat(Stream.make(void 0), Stream.fromSubscription(subscription)).pipe(Stream.mapEffect(() => Ref.get(cursor).pipe(Effect.flatMap((position) => store.read(record.id, position)), Effect.tap((events) => events.length === 0 ? Effect.void : Ref.set(cursor, events[events.length - 1].seq)))), Stream.flatMap(Stream.fromIterable));
|
|
160
|
-
}))
|
|
161
|
-
};
|
|
162
|
-
yield* Scope.addFinalizer(parent, close);
|
|
163
|
-
yield* Ref.update(handles, (all) => new Map([...all, [record.id, handle]]));
|
|
164
|
-
return handle;
|
|
165
|
-
}), opening = yield* Semaphore.make(1), ownRecord = (id) => store.get(id).pipe(Effect.flatMap((record) => record.workspace !== options.workspace ? Effect.fail(failure("session.workspace", "This session belongs to a different workspace")) : Effect.succeed(record)));
|
|
166
|
-
return {
|
|
167
|
-
create: () => Ref.get(graphRef).pipe(Effect.flatMap((graph) => store.create(options.workspace, graph.config.profile ?? "smith")), Effect.flatMap((record) => opening.withPermits(1)(open(record)))),
|
|
168
|
-
resume: (id) => ownRecord(id).pipe(Effect.flatMap((record) => opening.withPermits(1)(open(record)))),
|
|
169
|
-
fork: (id, through) => ownRecord(id).pipe(Effect.andThen(store.fork(id, through)), Effect.flatMap((record) => opening.withPermits(1)(open(record)))),
|
|
170
|
-
list: store.list(options.workspace),
|
|
171
|
-
graph: Ref.get(graphRef),
|
|
172
|
-
reconfigure: (config, plugins) => Effect.gen(function* () {
|
|
173
|
-
const registry = plugins ?? (yield* Ref.get(registryRef)), next = yield* resolveGraph(config, registry, [SessionEnvironment.key]);
|
|
174
|
-
if (graphFingerprint(next, "runtime") !== graphFingerprint(initial, "runtime"))
|
|
175
|
-
return "restart-required";
|
|
176
|
-
yield* Effect.forEach([...(yield* Ref.get(handles)).values()], (handle) => Effect.scoped(Effect.gen(function* () {
|
|
177
|
-
const scope = yield* Effect.scope, seed = Context.add(runtime, SessionEnvironment, { workspace: options.workspace, session: handle.record }), context = yield* activateGraph(next, "session", Context.makeUnsafe(seed.mapUnsafe), scope);
|
|
178
|
-
yield* service(context, AgentLoop);
|
|
179
|
-
})));
|
|
180
|
-
yield* Ref.set(registryRef, registry);
|
|
181
|
-
yield* Ref.set(graphRef, next);
|
|
182
|
-
yield* Effect.forEach([...(yield* Ref.get(handles)).values()], (handle) => handle.busy.pipe(Effect.flatMap((active) => active ? Effect.void : handle.refresh)));
|
|
183
|
-
return "applied";
|
|
184
|
-
})
|
|
185
|
-
};
|
|
186
|
-
}), Harness = { make: makeHarness };
|
package/dist/harness.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|