@xandreed/sdk 0.13.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/sdk
2
2
 
3
- Efferent composable agent framework, 0.13.0. Node 24 or Bun.
3
+ Efferent composable agent framework, 0.14.0. Node 24 or Bun.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
@@ -0,0 +1 @@
1
+ export {};
@@ -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
- /** Process-lifetime host services plugins may require. */
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
- /** Input tokens one request may use (system, tool schemas and messages). */
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 closed
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<TurnOutcome, HarnessError | E, Exclude<R, A | TurnServices | Scope.Scope>>;
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
  };
@@ -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}`), entryOf = (item) => ("plugin" in item) ? item : { plugin: item }, isWriter = (turn) => ("admitted" in turn), beginCodes = {
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
- }, beginTurn = (context, turn) => required(context, Sessions).pipe(Effect.flatMap((sessions) => sessions.begin(turn.session, {
36
- _tag: "User",
37
- userMessage: turn.userMessage,
38
- runId: turn.runId,
39
- key: turn.key ?? turn.runId,
40
- command: turn.command ?? {}
41
- })), Effect.mapError((error) => error instanceof HarnessError ? error : failure(beginCodes[error._tag], `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({
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
- }), outcome = 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));
88
- if (own)
89
- yield* writer.end({ reason: outcome.outcome, failure: Option.none() });
90
- return outcome;
91
- })));
92
- return { fingerprint: graphFingerprint(graph), turn };
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 output: unknown;
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 @@
1
+ export {};
@@ -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,3 +1,3 @@
1
1
  export * from "@xandreed/core";
2
- export { Agent } from "./agent.adapter.js";
2
+ export { Agent, turnBeginCodes } from "./agent.adapter.js";
3
3
  export type { AgentConfig, AgentPluginEntry } from "./agent.adapter.js";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from "@xandreed/core";
2
- 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,6 @@
1
1
  {
2
2
  "name": "@xandreed/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "engines": {
5
5
  "node": ">=24",
6
6
  "bun": ">=1.3.0"
@@ -14,9 +14,9 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@xandreed/core": "0.13.0",
17
+ "@xandreed/core": "0.14.0",
18
18
  "effect": "4.0.7",
19
- "@xandreed/runtime": "0.13.0"
19
+ "@xandreed/runtime": "0.14.0"
20
20
  },
21
21
  "private": false,
22
22
  "types": "./dist/index.d.ts",