@xandreed/sdk 0.11.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/sdk
2
2
 
3
- Efferent composable agent framework, 0.11.0. Node 24 or Bun.
3
+ Efferent composable agent framework, 0.13.0. Node 24 or Bun.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
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
@@ -1,5 +1,2 @@
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";
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@xandreed/sdk",
3
- "version": "0.11.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.11.0",
14
- "effect": "4.0.0-rc.118",
15
- "@xandreed/runtime": "0.11.0",
16
- "@xandreed/plugin-memory": "0.11.0",
17
- "@xandreed/plugin-session-sqlite": "0.11.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>;
@@ -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.11.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 };
@@ -1 +0,0 @@
1
- export {};