@xandreed/sdk 0.12.0 → 0.14.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/agent.adapter.d.ts +74 -6
- package/dist/agent.adapter.js +52 -17
- package/dist/agent.golden.test.d.ts +47 -6
- package/dist/events-guide.test.d.ts +207 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/journal-compat.test.d.ts +1 -0
- package/dist/models-plugin.test.d.ts +1 -0
- package/dist/resume.journey.test.d.ts +1 -0
- 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/harness.d.ts +0 -43
- package/dist/harness.js +0 -186
- /package/dist/{domain-session.adapter.test.d.ts → activity.test.d.ts} +0 -0
- /package/dist/{harness.test.d.ts → agent.trace.test.d.ts} +0 -0
package/README.md
CHANGED
package/dist/agent.adapter.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context, Effect } from "effect";
|
|
2
2
|
import type { Scope } from "effect";
|
|
3
3
|
import { HarnessError } from "@xandreed/core";
|
|
4
|
-
import type { Capability, LoopLimits, Plugin, Turn, TurnInput, TurnOutcome, TurnServices } from "@xandreed/core";
|
|
4
|
+
import type { Capability, LoopLimits, Plugin, ProcessActivity, Turn, TurnInput, TurnOutcome, TurnResult, TurnServices } from "@xandreed/core";
|
|
5
5
|
/** One plugin instance of an agent: the definition, its options and (optionally) an instance id. */
|
|
6
6
|
export interface AgentPluginEntry {
|
|
7
7
|
readonly plugin: Plugin;
|
|
@@ -17,7 +17,11 @@ export interface AgentConfig {
|
|
|
17
17
|
readonly bindings?: Readonly<Record<string, string>>;
|
|
18
18
|
/** The host's own tools, views, skills and prompt sections. */
|
|
19
19
|
readonly capabilities?: ReadonlyArray<Capability>;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Process-lifetime host services plugins may require. Their trace state
|
|
22
|
+
* is not replayed into turns (`replayableContext`): a parent span, span
|
|
23
|
+
* annotations or `CurrentTraceContent` among them are ignored.
|
|
24
|
+
*/
|
|
21
25
|
readonly services?: Context.Context<never>;
|
|
22
26
|
/** Keys every turn's services must carry; session plugins may require them. */
|
|
23
27
|
readonly turnServices?: ReadonlyArray<{
|
|
@@ -30,14 +34,52 @@ export interface AgentConfig {
|
|
|
30
34
|
readonly workspace?: string;
|
|
31
35
|
/** Defaults for every run; a turn's policy overrides them. */
|
|
32
36
|
readonly limits?: Partial<LoopLimits>;
|
|
33
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Every run's token budget unless its policy gives one; 64 000 by default.
|
|
39
|
+
* An integer of at least 1, checked here (`config.budget`). It is the
|
|
40
|
+
* target memory compaction keeps each request under (system prompt, tool
|
|
41
|
+
* schemas and messages, estimated), not a provider limit. Memory is given
|
|
42
|
+
* the whole budget at the turn's start, and at each step what the system
|
|
43
|
+
* prompt and tool schemas leave of it (`context.budget` when they leave
|
|
44
|
+
* nothing). The window strategy compacts, spills and drops older turns to
|
|
45
|
+
* fit the budget it is given, and fails with `context.budget` when the
|
|
46
|
+
* current turn alone does not; the summary strategy summarizes once the
|
|
47
|
+
* messages pass its `triggerRatio` of it; the digester ignores it. Each
|
|
48
|
+
* step's `context.built` records it.
|
|
49
|
+
*/
|
|
34
50
|
readonly budgetTokens?: number;
|
|
35
51
|
/** How deep event reactions may nest before the bus fails the publisher. */
|
|
36
52
|
readonly maxEventDepth?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Whether runs' spans may carry prompt and response content
|
|
55
|
+
* (`engine.step.input`, `engine.step.output`, the models plugin's
|
|
56
|
+
* `llm.reasoning`) when a policy does not say (`TurnPolicy.captureTraceContent`).
|
|
57
|
+
* Off by default, unless `CurrentTraceContent` is provided where a run
|
|
58
|
+
* starts: around `Agent.turn`, inside `use`, or in the turn's
|
|
59
|
+
* `TurnInput.services` (one in `services` is ignored: set this instead).
|
|
60
|
+
* Hosts own consent, masking and exporter retention.
|
|
61
|
+
*/
|
|
62
|
+
readonly captureTraceContent?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The keys of a turn's metadata (`NewTurn.meta`, `AdmittedTurn.meta`) its
|
|
65
|
+
* `agent.turn` span carries, as `efferent.meta.<key>`; none by default
|
|
66
|
+
* (see `TurnTraceOptions`).
|
|
67
|
+
*/
|
|
68
|
+
readonly spanMeta?: ReadonlyArray<string>;
|
|
37
69
|
}
|
|
38
70
|
/** A defined agent: its graph is built; each turn is composed by the host. */
|
|
39
71
|
export interface Agent {
|
|
40
72
|
readonly fingerprint: string;
|
|
73
|
+
/**
|
|
74
|
+
* What this process is busy with (`processActivity`), as the agent's
|
|
75
|
+
* runtime services see it: the `TurnActivity` of a runtime-scoped
|
|
76
|
+
* sessions plugin in its graph (or one in `services`), and the
|
|
77
|
+
* `TaskActivity` of a task runner in `services` (`InProcessTaskRunnerLive`
|
|
78
|
+
* built into them). A probe that is not there counts nothing: a Sessions
|
|
79
|
+
* the host passes only in a turn's services is not seen here (read
|
|
80
|
+
* `processActivity` with the host's own services instead).
|
|
81
|
+
*/
|
|
82
|
+
readonly activity: Effect.Effect<ProcessActivity>;
|
|
41
83
|
/**
|
|
42
84
|
* Run one turn: one the host began (its `TurnWriter`; the host ends it),
|
|
43
85
|
* or one begun and ended here through the `Sessions` in the turn's
|
|
@@ -45,14 +87,40 @@ export interface Agent {
|
|
|
45
87
|
* On success, tasks and background subscriptions settle as the scopes
|
|
46
88
|
* close in dependency order, before the reply is recorded exactly once.
|
|
47
89
|
* Failures and interruptions record a failed reply and cancel remaining
|
|
48
|
-
* scoped work; recorded events are stored before the turn returns. A turn
|
|
90
|
+
* scoped work; recorded events are stored before the turn returns. A turn
|
|
91
|
+
* begun here ends with `turnEndingOf` its exit: its outcome, or failed
|
|
92
|
+
* with the error's code (`turn.failed` for an error without one,
|
|
93
|
+
* `turn.defect`), or interrupted (`turn.interrupted`); on success, a
|
|
94
|
+
* failure to store that end fails the turn. A turn closed
|
|
49
95
|
* elsewhere (cancelled, reaped) fails with `turn.closed`. `use` runs in
|
|
50
96
|
* the turn's scope with the turn's services (RunContext, TurnEvents,
|
|
51
97
|
* TurnTasks and the input's `layer`) provided: the same instances the
|
|
52
|
-
* tools, policy and subscriptions see.
|
|
98
|
+
* tools, policy and subscriptions see. It returns `use`'s outcome with
|
|
99
|
+
* the turn's usage (every run's provider calls, summed). From the moment
|
|
100
|
+
* the turn is admitted it runs under its `agent.turn` span (`traceTurn`),
|
|
101
|
+
* and is counted in the `efferent.turns` and `efferent.turn.duration`
|
|
102
|
+
* metrics by its `efferent.outcome`.
|
|
53
103
|
*/
|
|
54
|
-
readonly turn: <A = never, E = never, R = never>(input: TurnInput<A, E>, use: (turn: Turn) => Effect.Effect<TurnOutcome, HarnessError, R>) => Effect.Effect<
|
|
104
|
+
readonly turn: <A = never, E = never, R = never>(input: TurnInput<A, E>, use: (turn: Turn) => Effect.Effect<TurnOutcome, HarnessError, R>) => Effect.Effect<TurnResult, HarnessError | E, Exclude<R, A | TurnServices | Scope.Scope>>;
|
|
55
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Why a turn could not begin (`Sessions.begin` failed, or, for
|
|
108
|
+
* `AttachmentRejected`, the attachments' check before it), as the code of
|
|
109
|
+
* the `HarnessError` `Agent.turn` fails with: keyed by the begin error's
|
|
110
|
+
* tag. An `AttachmentRejected`'s `reason` is in the message only; a host
|
|
111
|
+
* that branches on it runs `normalizeUserMessage` itself before the turn.
|
|
112
|
+
*/
|
|
113
|
+
export declare const turnBeginCodes: {
|
|
114
|
+
readonly AttachmentRejected: "turn.attachment-rejected";
|
|
115
|
+
readonly SessionBusy: "session.busy";
|
|
116
|
+
readonly TurnDuplicate: "turn.duplicate";
|
|
117
|
+
readonly KeyConflict: "turn.key-conflict";
|
|
118
|
+
readonly NothingPending: "session.nothing-pending";
|
|
119
|
+
readonly NotResumable: "turn.not-resumable";
|
|
120
|
+
readonly TurnRefused: "turn.refused";
|
|
121
|
+
readonly SessionMissing: "session.missing";
|
|
122
|
+
readonly SessionLogError: "session.log";
|
|
123
|
+
};
|
|
56
124
|
export declare const Agent: {
|
|
57
125
|
define: (config: AgentConfig) => Effect.Effect<Agent, HarnessError, Scope.Scope>;
|
|
58
126
|
};
|
package/dist/agent.adapter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Context, Effect, Option, Schema } from "effect";
|
|
1
|
+
import { Clock, Context, Duration, Effect, Exit, Metric, Option, Schema } from "effect";
|
|
2
2
|
import {
|
|
3
3
|
cacheKeyOf,
|
|
4
|
+
CurrentTraceContent,
|
|
4
5
|
CapabilitiesLive,
|
|
5
6
|
Capabilities,
|
|
6
7
|
ConversationMemory,
|
|
@@ -8,37 +9,63 @@ import {
|
|
|
8
9
|
guardTurn,
|
|
9
10
|
harnessDefectsAsFailures,
|
|
10
11
|
HarnessError,
|
|
12
|
+
isBudgetTokens,
|
|
13
|
+
normalizeUserMessage,
|
|
11
14
|
openTurnTools,
|
|
15
|
+
processActivity,
|
|
16
|
+
replayableContext,
|
|
12
17
|
SessionEnvironment,
|
|
13
18
|
Sessions,
|
|
14
19
|
StepLoop,
|
|
20
|
+
TaskActivity,
|
|
15
21
|
ToolRegistry,
|
|
16
22
|
RunContext,
|
|
17
23
|
settleTurn,
|
|
24
|
+
TurnActivity,
|
|
18
25
|
TurnEvents,
|
|
19
26
|
TurnTasks,
|
|
20
27
|
TurnLive,
|
|
21
28
|
TurnMemory,
|
|
22
29
|
TurnPrompt,
|
|
23
30
|
TurnToolbox,
|
|
31
|
+
traceTurn,
|
|
32
|
+
turnEndingOf,
|
|
24
33
|
turnOf
|
|
25
34
|
} from "@xandreed/core";
|
|
26
35
|
import { activateGraph, graphFingerprint, resolveGraph } from "@xandreed/runtime";
|
|
27
|
-
const failure = (code, message) => new HarnessError({ code, message }), missing = (key) => failure("service.missing", `The agent's plugins do not provide ${key}`),
|
|
36
|
+
const failure = (code, message) => new HarnessError({ code, message }), missing = (key) => failure("service.missing", `The agent's plugins do not provide ${key}`), turnCount = Metric.counter("efferent.turns", {
|
|
37
|
+
description: "turns run by Agent.turn, by outcome",
|
|
38
|
+
incremental: !0
|
|
39
|
+
}), turnDuration = Metric.timer("efferent.turn.duration", {
|
|
40
|
+
description: "how long each turn Agent.turn ran took, by outcome"
|
|
41
|
+
}), measureTurn = (effect) => Clock.currentTimeMillis.pipe(Effect.flatMap((started) => effect.pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
42
|
+
const outcome = { "efferent.outcome": turnEndingOf(exit).reason }, ended = yield* Clock.currentTimeMillis;
|
|
43
|
+
yield* Metric.update(Metric.withAttributes(turnCount, outcome), 1);
|
|
44
|
+
yield* Metric.update(Metric.withAttributes(turnDuration, outcome), Duration.millis(ended - started));
|
|
45
|
+
}))))), entryOf = (item) => ("plugin" in item) ? item : { plugin: item }, isWriter = (turn) => ("admitted" in turn);
|
|
46
|
+
export const turnBeginCodes = {
|
|
47
|
+
AttachmentRejected: "turn.attachment-rejected",
|
|
28
48
|
SessionBusy: "session.busy",
|
|
29
49
|
TurnDuplicate: "turn.duplicate",
|
|
30
50
|
KeyConflict: "turn.key-conflict",
|
|
31
51
|
NothingPending: "session.nothing-pending",
|
|
52
|
+
NotResumable: "turn.not-resumable",
|
|
32
53
|
TurnRefused: "turn.refused",
|
|
33
54
|
SessionMissing: "session.missing",
|
|
34
55
|
SessionLogError: "session.log"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
userMessage
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
};
|
|
57
|
+
const keyOf = (turn) => turn.key ?? (turn.resumes === void 0 ? turn.runId : `resume:${turn.resumes}`), beginTurn = (context, turn) => Effect.gen(function* () {
|
|
58
|
+
const sessions = yield* required(context, Sessions), userMessage = yield* normalizeUserMessage(turn.userMessage);
|
|
59
|
+
return yield* sessions.begin(turn.session, {
|
|
60
|
+
_tag: "User",
|
|
61
|
+
userMessage,
|
|
62
|
+
runId: turn.runId,
|
|
63
|
+
key: keyOf(turn),
|
|
64
|
+
command: turn.command ?? {},
|
|
65
|
+
...turn.meta === void 0 ? {} : { meta: turn.meta },
|
|
66
|
+
...turn.resumes === void 0 ? {} : { resumes: turn.resumes }
|
|
67
|
+
});
|
|
68
|
+
}).pipe(Effect.mapError((error) => error instanceof HarnessError ? error : failure(turnBeginCodes[error._tag], error._tag === "NotResumable" ? `turn ${error.turn} of session ${turn.session.id} cannot be resumed (${error.reason})` : error._tag === "AttachmentRejected" ? `the turn could not begin (AttachmentRejected, ${error.reason}: ${error.message}) in session ${turn.session.id}` : `the turn could not begin (${error._tag}) in session ${turn.session.id}`))), untilClosed = (writer) => (body) => Effect.raceFirst(body, writer.closed.pipe(Effect.flatMap((closed) => Effect.fail(failure("turn.closed", `turn ${closed.turn} was ${closed.reason}`))))), required = (context, tag) => Option.match(Context.getOption(context, tag), { onNone: () => Effect.fail(missing(tag.key)), onSome: Effect.succeed }), provideHostLayer = (layer) => (effect) => Option.match(layer, { onNone: () => effect, onSome: (hostLayer) => effect.pipe(Effect.provide(hostLayer, { local: !0 })) }), hostCapabilities = (capabilities) => definePlugin({
|
|
42
69
|
id: "@xandreed/sdk/host-contributions",
|
|
43
70
|
version: "1",
|
|
44
71
|
scope: "runtime",
|
|
@@ -50,6 +77,8 @@ const failure = (code, message) => new HarnessError({ code, message }), missing
|
|
|
50
77
|
}), builtInTurnKeys = [RunContext, TurnEvents, TurnTasks, TurnMemory, TurnPrompt, TurnToolbox].map((tag) => tag.key), turnDependentNodes = (graph) => graph.nodes.reduce((late, node) => {
|
|
51
78
|
return [...node.plugin.requires, ...node.plugin.optional ?? []].some((key) => builtInTurnKeys.includes(key) || late.some((source) => source.plugin.provides.includes(key) || (source.plugin.contributes ?? []).includes(key))) ? [...late, node] : late;
|
|
52
79
|
}, []), makeAgent = (config) => Effect.gen(function* () {
|
|
80
|
+
if (config.budgetTokens !== void 0 && !isBudgetTokens(config.budgetTokens))
|
|
81
|
+
return yield* Effect.fail(failure("config.budget", `budgetTokens must be an integer of at least 1, not ${config.budgetTokens}`));
|
|
53
82
|
const parent = yield* Effect.scope, entries = config.plugins.map(entryOf), hosted = config.capabilities ?? [], host = hostCapabilities(hosted), withHost = hosted.length > 0, plugins = [...withHost ? [host] : [], ...entries.map((entry) => entry.plugin)], harness = {
|
|
54
83
|
version: 1,
|
|
55
84
|
plugins: [
|
|
@@ -65,7 +94,7 @@ const failure = (code, message) => new HarnessError({ code, message }), missing
|
|
|
65
94
|
const scope = yield* Effect.scope, absent = turnKeys.filter((key) => !input.services.mapUnsafe.has(key));
|
|
66
95
|
if (absent.length > 0)
|
|
67
96
|
return yield* Effect.fail(failure("service.missing", `The turn does not provide ${absent.join(", ")}`));
|
|
68
|
-
const merged = Context.merge(runtime, input.services), context = perTurn ? yield* activateGraph(beforeTurn, "session", merged, scope) : merged, memory = yield* required(context, ConversationMemory), registry = yield* required(context, ToolRegistry), loop = yield* required(context, StepLoop), own = !isWriter(input.turn), writer = isWriter(input.turn) ? input.turn : yield* beginTurn(context, input.turn), system = input.system ?? config.system, live = {
|
|
97
|
+
const merged = Context.merge(replayableContext(Context.merge(runtime, input.services)), Context.pick(CurrentTraceContent)(input.services)), context = perTurn ? yield* activateGraph(beforeTurn, "session", merged, scope) : merged, memory = yield* required(context, ConversationMemory), registry = yield* required(context, ToolRegistry), loop = yield* required(context, StepLoop), own = !isWriter(input.turn), writer = isWriter(input.turn) ? input.turn : yield* beginTurn(context, input.turn), system = input.system ?? config.system, live = {
|
|
69
98
|
turn: writer,
|
|
70
99
|
...system === void 0 ? {} : { system },
|
|
71
100
|
...config.maxEventDepth === void 0 ? {} : { maxEventDepth: config.maxEventDepth }
|
|
@@ -73,7 +102,8 @@ const failure = (code, message) => new HarnessError({ code, message }), missing
|
|
|
73
102
|
...config.limits === void 0 ? {} : { limits: config.limits },
|
|
74
103
|
...config.budgetTokens === void 0 ? {} : { budgetTokens: config.budgetTokens },
|
|
75
104
|
cacheKey: Option.orElse(Option.fromNullishOr(input.cacheKey), () => cacheKeyOf(config.cacheKeyPrefix ?? "", writer.admitted.session.id)),
|
|
76
|
-
...input.steering === void 0 ? {} : { steering: input.steering }
|
|
105
|
+
...input.steering === void 0 ? {} : { steering: input.steering },
|
|
106
|
+
...config.captureTraceContent === void 0 ? {} : { captureTraceContent: config.captureTraceContent }
|
|
77
107
|
}, body = Effect.gen(function* () {
|
|
78
108
|
yield* (yield* TurnMemory).persistMessage;
|
|
79
109
|
yield* openTurnTools;
|
|
@@ -84,11 +114,16 @@ const failure = (code, message) => new HarnessError({ code, message }), missing
|
|
|
84
114
|
const activated = yield* activateGraph(afterTurn, "session", Context.merge(context, liveContext), yield* Effect.scope);
|
|
85
115
|
return yield* effect.pipe(Effect.provide(activated));
|
|
86
116
|
}));
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
yield*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
});
|
|
118
|
+
return yield* Effect.gen(function* () {
|
|
119
|
+
const exit = yield* body.pipe(Effect.tap(() => settleTurn), Effect.scoped, Effect.tap(() => settleTurn), provideHostLayer(Option.fromNullishOr(input.layer)), Effect.tap(() => settleTurn), provideTurnPlugins, guardTurn, Effect.provide(TurnLive(live), { local: !0 }), Effect.provideService(ConversationMemory, memory), Effect.provideService(ToolRegistry, registry), Effect.provideService(StepLoop, loop), Effect.provide(context), untilClosed(writer), harnessDefectsAsFailures, Effect.exit);
|
|
120
|
+
if (own && Exit.isSuccess(exit))
|
|
121
|
+
yield* writer.end(turnEndingOf(exit));
|
|
122
|
+
if (own && Exit.isFailure(exit))
|
|
123
|
+
yield* writer.end(turnEndingOf(exit)).pipe(Effect.ignore);
|
|
124
|
+
return yield* exit;
|
|
125
|
+
}).pipe(measureTurn, traceTurn(writer, { spanMeta: config.spanMeta ?? [] }));
|
|
126
|
+
}))), activity = processActivity.pipe(Effect.updateContext((current) => Context.merge(Context.omit(TurnActivity, TaskActivity)(current), Context.pick(TurnActivity, TaskActivity)(runtime))));
|
|
127
|
+
return { fingerprint: graphFingerprint(graph), activity, turn };
|
|
93
128
|
});
|
|
94
129
|
export const Agent = { define: makeAgent };
|
|
@@ -68,6 +68,15 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
68
68
|
} | {
|
|
69
69
|
readonly _tag: "TurnStarted";
|
|
70
70
|
readonly prompt: string;
|
|
71
|
+
readonly attachments?: readonly {
|
|
72
|
+
readonly type: "media";
|
|
73
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
74
|
+
readonly data: string;
|
|
75
|
+
readonly fingerprint: string;
|
|
76
|
+
readonly bytes: number;
|
|
77
|
+
readonly name?: string;
|
|
78
|
+
readonly alt?: string;
|
|
79
|
+
}[];
|
|
71
80
|
} | {
|
|
72
81
|
readonly _tag: "TurnContext";
|
|
73
82
|
readonly sectionId: string;
|
|
@@ -77,7 +86,19 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
77
86
|
readonly _tag: "Message";
|
|
78
87
|
readonly message: {
|
|
79
88
|
readonly role: "user";
|
|
80
|
-
readonly content: string
|
|
89
|
+
readonly content: string | readonly ({
|
|
90
|
+
readonly type: "media";
|
|
91
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
92
|
+
readonly data: string;
|
|
93
|
+
readonly fingerprint: string;
|
|
94
|
+
readonly bytes: number;
|
|
95
|
+
readonly name?: string;
|
|
96
|
+
readonly alt?: string;
|
|
97
|
+
} | {
|
|
98
|
+
readonly type: "text";
|
|
99
|
+
readonly text: string;
|
|
100
|
+
readonly providerOptions?: unknown;
|
|
101
|
+
})[];
|
|
81
102
|
readonly providerOptions?: unknown;
|
|
82
103
|
} | {
|
|
83
104
|
readonly role: "assistant";
|
|
@@ -91,9 +112,9 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
91
112
|
readonly providerOptions?: unknown;
|
|
92
113
|
} | {
|
|
93
114
|
readonly type: "tool-call";
|
|
115
|
+
readonly input: unknown;
|
|
94
116
|
readonly toolCallId: string;
|
|
95
117
|
readonly toolName: string;
|
|
96
|
-
readonly input: unknown;
|
|
97
118
|
readonly providerOptions?: unknown;
|
|
98
119
|
readonly providerExecuted?: boolean | undefined;
|
|
99
120
|
})[];
|
|
@@ -102,9 +123,18 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
102
123
|
readonly role: "tool";
|
|
103
124
|
readonly content: readonly {
|
|
104
125
|
readonly type: "tool-result";
|
|
126
|
+
readonly output: unknown;
|
|
105
127
|
readonly toolCallId: string;
|
|
106
128
|
readonly toolName: string;
|
|
107
|
-
readonly
|
|
129
|
+
readonly media?: readonly {
|
|
130
|
+
readonly type: "media";
|
|
131
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
132
|
+
readonly data: string;
|
|
133
|
+
readonly fingerprint: string;
|
|
134
|
+
readonly bytes: number;
|
|
135
|
+
readonly name?: string;
|
|
136
|
+
readonly alt?: string;
|
|
137
|
+
}[];
|
|
108
138
|
readonly providerOptions?: unknown;
|
|
109
139
|
readonly isError?: boolean | undefined;
|
|
110
140
|
}[];
|
|
@@ -115,9 +145,6 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
115
145
|
readonly toolCallId: string;
|
|
116
146
|
readonly toolName: string;
|
|
117
147
|
readonly isError: boolean;
|
|
118
|
-
readonly encoded: unknown;
|
|
119
|
-
readonly view: string;
|
|
120
|
-
readonly viewVersion: string;
|
|
121
148
|
readonly subjects: readonly {
|
|
122
149
|
readonly kind: string;
|
|
123
150
|
readonly id: string;
|
|
@@ -131,7 +158,17 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
131
158
|
readonly url: string;
|
|
132
159
|
readonly alt: string | null;
|
|
133
160
|
}[];
|
|
161
|
+
readonly encoded: unknown;
|
|
162
|
+
readonly view: string;
|
|
163
|
+
readonly viewVersion: string;
|
|
134
164
|
readonly pinned: boolean;
|
|
165
|
+
readonly media?: readonly {
|
|
166
|
+
readonly path: readonly (string | number)[];
|
|
167
|
+
readonly fingerprint: string;
|
|
168
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
169
|
+
readonly bytes: number;
|
|
170
|
+
}[];
|
|
171
|
+
readonly mediaNotes?: readonly string[];
|
|
135
172
|
} | {
|
|
136
173
|
readonly _tag: "ToolDigest";
|
|
137
174
|
readonly entry: string;
|
|
@@ -171,6 +208,10 @@ export declare const runGolden: (runner: Effect.Effect<Pick<Agent, "turn">, Harn
|
|
|
171
208
|
readonly _tag: "Summarize";
|
|
172
209
|
readonly keepFromTurn: number;
|
|
173
210
|
readonly summary: string;
|
|
211
|
+
} | {
|
|
212
|
+
readonly _tag: "OmitMedia";
|
|
213
|
+
readonly entries: readonly string[];
|
|
214
|
+
readonly reason: string;
|
|
174
215
|
};
|
|
175
216
|
} | {
|
|
176
217
|
readonly _tag: "TurnEnded";
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Layer, Option, Schema, Scope, Stream } from "effect";
|
|
2
|
+
import { LanguageModel, Tool } from "effect/ai";
|
|
3
|
+
import { Agent, ConversationId, HarnessError, Sessions, TurnEvents } from "@xandreed/sdk";
|
|
4
|
+
import type { Capability, SessionAddress, SessionLog, Turn, TurnEvent, TurnEventOf, TurnEventsService, TurnOutcome } from "@xandreed/sdk";
|
|
5
|
+
import type { FeedOptions, FeedProjection, SsePeer } from "@xandreed/plugin-render";
|
|
6
|
+
/** What a server instance is built from. */
|
|
7
|
+
export interface HostOptions {
|
|
8
|
+
/** The session store every instance shares (your database; `SessionLogMemoryLive` is one process's). */
|
|
9
|
+
readonly log: Layer.Layer<SessionLog>;
|
|
10
|
+
/** From `@xandreed/plugin-models`, or any effect/ai provider. */
|
|
11
|
+
readonly model: LanguageModel.LanguageModel;
|
|
12
|
+
readonly capabilities?: ReadonlyArray<Capability>;
|
|
13
|
+
readonly feed?: FeedOptions;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One server instance: the agent, the services it was given (the Sessions
|
|
17
|
+
* it writes through, and their activity probe), the journal feed and the model.
|
|
18
|
+
*/
|
|
19
|
+
export declare const makeHost: (options: HostOptions) => Effect.Effect<{
|
|
20
|
+
agent: Agent;
|
|
21
|
+
services: Context.Context<Sessions | import("@xandreed/core").TurnActivity>;
|
|
22
|
+
sessions: {
|
|
23
|
+
readonly create: (input: {
|
|
24
|
+
readonly owner: string;
|
|
25
|
+
readonly id?: ConversationId;
|
|
26
|
+
readonly origin?: string;
|
|
27
|
+
readonly meta?: import("@xandreed/core").JsonObject;
|
|
28
|
+
}) => Effect.Effect<import("@xandreed/core").SessionView, import("@xandreed/core").SessionExists | import("@xandreed/core").SessionLogError>;
|
|
29
|
+
readonly fork: (parent: SessionAddress, input: {
|
|
30
|
+
readonly id?: ConversationId;
|
|
31
|
+
readonly origin: string;
|
|
32
|
+
readonly meta?: import("@xandreed/core").JsonObject;
|
|
33
|
+
readonly inherit: boolean;
|
|
34
|
+
}) => Effect.Effect<import("@xandreed/core").SessionView, import("@xandreed/core").SessionExists | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
35
|
+
readonly get: (address: SessionAddress) => Effect.Effect<import("@xandreed/core").SessionView, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
36
|
+
readonly list: (query: {
|
|
37
|
+
readonly owner: string;
|
|
38
|
+
readonly limit?: number;
|
|
39
|
+
readonly before?: import("@xandreed/core").SessionListCursor;
|
|
40
|
+
readonly parent?: ConversationId;
|
|
41
|
+
}) => Effect.Effect<import("@xandreed/core").SessionPage, import("@xandreed/core").SessionLogError>;
|
|
42
|
+
readonly remove: (address: SessionAddress) => Effect.Effect<void, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
43
|
+
readonly read: (address: SessionAddress, query?: {
|
|
44
|
+
readonly after?: number;
|
|
45
|
+
readonly kinds?: ReadonlyArray<string>;
|
|
46
|
+
readonly limit?: number;
|
|
47
|
+
}) => Effect.Effect<ReadonlyArray<import("@xandreed/core").SessionLogEvent>, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
48
|
+
readonly changes: (address: SessionAddress) => Stream.Stream<void>;
|
|
49
|
+
readonly lookup: (address: SessionAddress, key: string) => Effect.Effect<Option.Option<number>, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
50
|
+
readonly transact: <A, E>(address: SessionAddress, decide: (view: import("@xandreed/core").SessionView) => Effect.Effect<import("@xandreed/core").Decision<A>, E>) => Effect.Effect<import("@xandreed/core").Decided<A>, E | import("@xandreed/core").ReservedKind | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
51
|
+
readonly cancel: (address: SessionAddress, turn: number) => Effect.Effect<{
|
|
52
|
+
readonly cancelled: boolean;
|
|
53
|
+
readonly pending: number;
|
|
54
|
+
}, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
55
|
+
readonly begin: (address: SessionAddress, input: import("@xandreed/core").BeginTurn) => Effect.Effect<import("@xandreed/core").TurnWriter, import("@xandreed/core").AttachmentRejected | import("@xandreed/core").SessionBusy | import("@xandreed/core").TurnDuplicate | import("@xandreed/core").KeyConflict | import("@xandreed/core").NothingPending | import("@xandreed/core").NotResumable | import("@xandreed/core").TurnRefused | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError, Scope.Scope>;
|
|
56
|
+
readonly deliver: (address: SessionAddress, item: import("@xandreed/core").InboxItem) => Effect.Effect<{
|
|
57
|
+
readonly delivered: boolean;
|
|
58
|
+
readonly pending: number;
|
|
59
|
+
}, import("@xandreed/core").InboxFull | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError>;
|
|
60
|
+
readonly drain: <E, R>(address: SessionAddress, run: (writer: import("@xandreed/core").TurnWriter) => Effect.Effect<void, E, R>, options?: {
|
|
61
|
+
readonly maxTurns?: number;
|
|
62
|
+
readonly meta?: import("@xandreed/core").JsonObject;
|
|
63
|
+
}) => Effect.Effect<{
|
|
64
|
+
readonly turns: number;
|
|
65
|
+
}, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError, R>;
|
|
66
|
+
};
|
|
67
|
+
feed: {
|
|
68
|
+
readonly frames: (feed: import("@xandreed/plugin-render").FeedScope, after: number, project: FeedProjection) => Stream.Stream<import("@xandreed/plugin-render").FeedFrame, import("@xandreed/plugin-render").RenderError>;
|
|
69
|
+
};
|
|
70
|
+
model: LanguageModel.LanguageModel;
|
|
71
|
+
}, HarnessError, Scope.Scope>;
|
|
72
|
+
export type Host = Effect.Success<ReturnType<typeof makeHost>>;
|
|
73
|
+
/** Built once per server instance (a module-level promise in Next.js); its scope is never closed. */
|
|
74
|
+
export declare const startHost: (options: HostOptions) => Promise<Host>;
|
|
75
|
+
/** One SSE frame for the client: an event name and its JSON data. */
|
|
76
|
+
export interface Frame {
|
|
77
|
+
readonly event: string;
|
|
78
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
79
|
+
}
|
|
80
|
+
/** The bus events a client is shown, as frames; `None` skips one. Tool input and output stay on the server. */
|
|
81
|
+
export declare const progressOf: (event: TurnEvent) => Option.Option<Frame>;
|
|
82
|
+
/** Sends one frame to the client. It must never fail (see `clientPush`). */
|
|
83
|
+
export type Push = (frame: Frame) => Effect.Effect<void>;
|
|
84
|
+
/**
|
|
85
|
+
* `use` for a turn streamed to a client: one background subscription (one
|
|
86
|
+
* ordered lane, off the loop's path) for every frame, each tagged with the
|
|
87
|
+
* turn's number, then the run.
|
|
88
|
+
*/
|
|
89
|
+
export declare const streamTo: (push: Push) => (turn: Turn) => Effect.Effect<TurnOutcome, HarnessError, Scope.Scope>;
|
|
90
|
+
/** Whether the client took `bytes` within `timeout`: a write still waiting then, or one that failed (the client left), was not. */
|
|
91
|
+
export declare const writeWithin: (writer: WritableStreamDefaultWriter<Uint8Array>, bytes: Uint8Array, timeout: Duration.Input) => Effect.Effect<boolean, never, never>;
|
|
92
|
+
/**
|
|
93
|
+
* Ends a client's stream without waiting: a stream aborted while a write
|
|
94
|
+
* still waits settles neither until the client reads again or leaves.
|
|
95
|
+
*/
|
|
96
|
+
export declare const closeStream: (writer: WritableStreamDefaultWriter<Uint8Array>) => Effect.Effect<Promise<undefined>, never, never>;
|
|
97
|
+
/**
|
|
98
|
+
* A client's stream as a `Push` that never fails and never holds the turn
|
|
99
|
+
* up: a write the client has not taken within `timeout` marks it gone and
|
|
100
|
+
* aborts the stream, and every later frame is dropped. A handler that fails
|
|
101
|
+
* fails the turn; one that never returns keeps it from recording its reply.
|
|
102
|
+
*/
|
|
103
|
+
export declare const clientPush: (writer: WritableStreamDefaultWriter<Uint8Array>, timeout?: Duration.Input) => Effect.Effect<Push, never, never>;
|
|
104
|
+
/** For the turn's `layer`: a subscription there sees `turn.started` (with `meta` and `resumes`), which `use` never does. */
|
|
105
|
+
export declare const onTurnStarted: (handle: (event: TurnEventOf<"turn.started">) => Effect.Effect<void>) => Layer.Layer<never, never, TurnEvents>;
|
|
106
|
+
/**
|
|
107
|
+
* From `use`: a subscription moved into `outer`, a scope the caller keeps
|
|
108
|
+
* open past the turn, the only kind that sees `turn.ended`. Its handler runs
|
|
109
|
+
* after the turn's own scopes have closed, and must not fail.
|
|
110
|
+
*/
|
|
111
|
+
export declare const onTurnEnded: (outer: Scope.Scope, handle: (event: TurnEventOf<"turn.ended">) => Effect.Effect<void>) => (turn: Turn) => Effect.Effect<void, never, never>;
|
|
112
|
+
export declare const Lookup: Tool.Tool<"lookup", {
|
|
113
|
+
readonly parameters: Schema.Struct<{
|
|
114
|
+
readonly query: Schema.String;
|
|
115
|
+
}>;
|
|
116
|
+
readonly success: Schema.Struct<{
|
|
117
|
+
readonly found: Schema.Number;
|
|
118
|
+
}>;
|
|
119
|
+
readonly failure: Schema.Struct<{
|
|
120
|
+
readonly error: Schema.String;
|
|
121
|
+
readonly message: Schema.String;
|
|
122
|
+
}>;
|
|
123
|
+
readonly failureMode: "return";
|
|
124
|
+
}, never>;
|
|
125
|
+
/** Typed reactions to `lookup`'s successful calls, and a line for every call refused or failed. */
|
|
126
|
+
export declare const watchTools: (turn: Turn, report: (line: string) => Effect.Effect<void>) => Effect.Effect<void, never, Scope.Scope>;
|
|
127
|
+
/** The application's own event: typed by its schema, journaled under its name, seen by the session's followers. */
|
|
128
|
+
export declare const Progress: {
|
|
129
|
+
name: string;
|
|
130
|
+
make: (data: {
|
|
131
|
+
readonly stage: string;
|
|
132
|
+
readonly done: number;
|
|
133
|
+
}) => Effect.Effect<TurnEvent, HarnessError>;
|
|
134
|
+
select: (event: TurnEvent) => Option.Option<{
|
|
135
|
+
readonly stage: string;
|
|
136
|
+
readonly done: number;
|
|
137
|
+
}>;
|
|
138
|
+
publish: (events: TurnEventsService, data: {
|
|
139
|
+
readonly stage: string;
|
|
140
|
+
readonly done: number;
|
|
141
|
+
}) => Effect.Effect<void, HarnessError, never>;
|
|
142
|
+
on: <R>(handle: (data: {
|
|
143
|
+
readonly stage: string;
|
|
144
|
+
readonly done: number;
|
|
145
|
+
}) => Effect.Effect<void, HarnessError, R>, options?: import("@xandreed/core").SubscribeOptions) => import("@xandreed/core").Subscription<R>;
|
|
146
|
+
};
|
|
147
|
+
/** Publish it from `use` (`turn.events`), a task, a tool (`RunContext.events`) or a session plugin (`TurnEvents`). */
|
|
148
|
+
export declare const reportStage: (events: TurnEventsService, stage: string, done: number) => Effect.Effect<void, HarnessError, never>;
|
|
149
|
+
/** The events stored after `cursor`, oldest first, and the cursor to read from next. */
|
|
150
|
+
export declare const readAfter: (sessions: typeof Sessions.Service, address: SessionAddress, cursor: number) => Effect.Effect<{
|
|
151
|
+
events: readonly {
|
|
152
|
+
readonly session: string & import("effect/Brand").Brand<"ConversationId">;
|
|
153
|
+
readonly seq: number;
|
|
154
|
+
readonly turn: Option.Option<number>;
|
|
155
|
+
readonly kind: string;
|
|
156
|
+
readonly at: number;
|
|
157
|
+
readonly data: {
|
|
158
|
+
readonly [x: string]: unknown;
|
|
159
|
+
};
|
|
160
|
+
}[];
|
|
161
|
+
cursor: number;
|
|
162
|
+
}, import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError, never>;
|
|
163
|
+
/** How each turn of a session ended, as stored: the reason and, when it failed, the failing error's code. */
|
|
164
|
+
export declare const endingsOf: (sessions: typeof Sessions.Service, address: SessionAddress) => Effect.Effect<{
|
|
165
|
+
turn: number | null;
|
|
166
|
+
reason: "partial" | "completed" | "failed" | "cancelled" | "interrupted";
|
|
167
|
+
code: string | null;
|
|
168
|
+
}[], Schema.SchemaError | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError, never>;
|
|
169
|
+
/** How a turn ended and what it replied, as the journal has it. */
|
|
170
|
+
export interface StoredAnswer {
|
|
171
|
+
readonly turn: number;
|
|
172
|
+
readonly reason: string;
|
|
173
|
+
readonly reply: string | null;
|
|
174
|
+
}
|
|
175
|
+
/** What the turn begun with `key` answered, from the journal: none until that turn has ended. */
|
|
176
|
+
export declare const storedAnswer: (sessions: typeof Sessions.Service, address: SessionAddress, key: string) => Effect.Effect<Option.None<StoredAnswer> | Option.Some<StoredAnswer>, Schema.SchemaError | import("@xandreed/core").SessionMissing | import("@xandreed/core").SessionLogError, never>;
|
|
177
|
+
/** The journal as a client sees it: each shown record with its turn, and the app's own `app.*` events as they are. */
|
|
178
|
+
export declare const clientProjection: FeedProjection;
|
|
179
|
+
export declare const statusOf: (code: string) => number;
|
|
180
|
+
export declare const problem: (status: number, code: string) => Response;
|
|
181
|
+
/**
|
|
182
|
+
* POST /sessions/:id/turns, `{ text, resumes? }` with an `Idempotency-Key`
|
|
183
|
+
* header: the turn as SSE frames, `started` first and `done` last. A turn
|
|
184
|
+
* that cannot begin is answered with its code's status before anything
|
|
185
|
+
* streams. `waitUntil` keeps the turn running after the client leaves.
|
|
186
|
+
*/
|
|
187
|
+
export declare const turnRoute: (host: Host, owner: string, id: string, request: Request, waitUntil: (work: Promise<unknown>) => void) => Promise<Response>;
|
|
188
|
+
/**
|
|
189
|
+
* POST /sessions, `{ id? }`: a new session, 201. A client that names the id
|
|
190
|
+
* may retry: its own session again is 200, an id another owner holds is 409.
|
|
191
|
+
*/
|
|
192
|
+
export declare const createSessionRoute: (host: Host, owner: string, request: Request) => Promise<Response>;
|
|
193
|
+
/**
|
|
194
|
+
* An SSE peer over a Web stream: a write never fails, and `closed` completes
|
|
195
|
+
* when the client leaves, or once a write has waited `timeout` on a client
|
|
196
|
+
* that stopped reading: the stream is then aborted, and the feed stops.
|
|
197
|
+
*/
|
|
198
|
+
export declare const webSsePeer: (writer: WritableStreamDefaultWriter<Uint8Array>, signal: AbortSignal, timeout?: Duration.Input) => SsePeer;
|
|
199
|
+
/**
|
|
200
|
+
* GET /sessions/:id/events: the session's journal from the client's cursor
|
|
201
|
+
* as SSE (`id: <seq>`, `event: <kind>`), polled, and woken by this
|
|
202
|
+
* instance's own commits, until the feed's `maxDurationMs` or a client that
|
|
203
|
+
* stopped reading is dropped; EventSource then reconnects with `Last-Event-ID`.
|
|
204
|
+
*/
|
|
205
|
+
export declare const eventsRoute: (host: Host, owner: string, id: string, request: Request, waitUntil: (work: Promise<unknown>) => void) => Promise<Response>;
|
|
206
|
+
/** GET /ping: what this instance has in flight, for a load balancer, or a deploy waiting to stop it. */
|
|
207
|
+
export declare const pingRoute: (host: Host) => Promise<Response>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
export * from "@xandreed/core";
|
|
2
|
-
export
|
|
3
|
-
export * from "./approval.adapter.js";
|
|
4
|
-
export { domainLoop, domainSession } from "./domain-session.adapter.js";
|
|
5
|
-
export { Agent } from "./agent.adapter.js";
|
|
2
|
+
export { Agent, turnBeginCodes } 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
|
|
3
|
-
export * from "./approval.adapter.js";
|
|
4
|
-
export { domainLoop, domainSession } from "./domain-session.adapter.js";
|
|
5
|
-
export { Agent } from "./agent.adapter.js";
|
|
2
|
+
export { Agent, turnBeginCodes } from "./agent.adapter.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.14.0",
|
|
18
|
+
"effect": "4.0.7",
|
|
19
|
+
"@xandreed/runtime": "0.14.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
|
-
};
|
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 };
|
|
File without changes
|
|
File without changes
|