@xandreed/plugin-agent-loop 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/plugin-agent-loop
2
2
 
3
- Efferent composable agent framework, 0.11.0. Node 24 or Bun.
3
+ Efferent composable agent framework, 0.13.0. Node 24 or Bun.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
package/dist/index.d.ts CHANGED
@@ -1,4 +1,2 @@
1
1
  export * from "./loop.js";
2
- export * from "./runAgent.js";
3
- export { agentLoopPlugin, default } from "./plugin.adapter.js";
4
- export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin } from "./step-loop.adapter.js";
2
+ export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin, stepLoopPlugin as default } from "./step-loop.adapter.js";
package/dist/index.js CHANGED
@@ -1,4 +1,2 @@
1
1
  export * from "./loop.js";
2
- export * from "./runAgent.js";
3
- export { agentLoopPlugin, default } from "./plugin.adapter.js";
4
- export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin } from "./step-loop.adapter.js";
2
+ export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin, stepLoopPlugin as default } from "./step-loop.adapter.js";
@@ -83,7 +83,7 @@ export const runSteps = (request) => Effect.gen(function* () {
83
83
  };
84
84
  }), StepLoopLive = Layer.succeed(StepLoop, StepLoop.of({ ...STEP_LOOP, run: runSteps })), stepLoopPlugin = definePlugin({
85
85
  id: "@xandreed/plugin-agent-loop/steps",
86
- version: "0.11.0",
86
+ version: "0.13.0",
87
87
  scope: "runtime",
88
88
  config: Schema.Struct({}),
89
89
  defaults: {},
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@xandreed/plugin-agent-loop",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
+ "engines": {
5
+ "node": ">=24",
6
+ "bun": ">=1.3.0"
7
+ },
4
8
  "type": "module",
5
9
  "main": "./dist/index.js",
6
10
  "exports": {
@@ -10,8 +14,8 @@
10
14
  }
11
15
  },
12
16
  "dependencies": {
13
- "@xandreed/core": "0.11.0",
14
- "effect": "4.0.0-rc.118"
17
+ "@xandreed/core": "0.13.0",
18
+ "effect": "4.0.7"
15
19
  },
16
20
  "private": false,
17
21
  "types": "./dist/index.d.ts",
@@ -22,9 +26,5 @@
22
26
  "README.md",
23
27
  "LICENSE"
24
28
  ],
25
- "engines": {
26
- "node": ">=24",
27
- "bun": ">=1.3.0"
28
- },
29
29
  "license": "MIT"
30
30
  }
@@ -1,12 +0,0 @@
1
- import { LanguageModel } from "effect/ai";
2
- import { AgentLoop, AgentTools, ContextManager, Memory, SessionStore } from "@xandreed/core";
3
- export declare const agentLoopPlugin: import("@xandreed/core").TypedPlugin<{
4
- readonly maxSteps: number;
5
- readonly toolConcurrency: number;
6
- readonly streaming: boolean;
7
- }, {
8
- readonly maxSteps: number;
9
- readonly toolConcurrency: number;
10
- readonly streaming: boolean;
11
- }, AgentLoop, never, LanguageModel.LanguageModel | SessionStore | AgentTools | ContextManager | Memory>;
12
- export default agentLoopPlugin;
@@ -1,54 +0,0 @@
1
- import { LanguageModel } from "effect/ai";
2
- import { Effect, Layer, Option, Ref, Schema } from "effect";
3
- import { AgentLoop, AgentMessage, AgentTools, ContextManager, definePlugin, handoffToMessage, HarnessError, Memory, SessionStore } from "@xandreed/core";
4
- import { runLoop } from "./loop.js";
5
- const Config = Schema.Struct({ maxSteps: Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: 1000 }))), toolConcurrency: Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: 32 }))), streaming: Schema.Boolean });
6
- export const agentLoopPlugin = definePlugin({
7
- id: "@xandreed/plugin-agent-loop",
8
- version: "0.11.0",
9
- config: Config,
10
- defaults: { maxSteps: 100, toolConcurrency: 1, streaming: !0 },
11
- requires: [LanguageModel.LanguageModel, AgentTools, SessionStore, Memory, ContextManager],
12
- provides: [AgentLoop],
13
- layer: (config) => Layer.effect(AgentLoop, Effect.gen(function* () {
14
- const model = yield* LanguageModel.LanguageModel, tools = yield* AgentTools, store = yield* SessionStore, memory = yield* Memory, context = yield* ContextManager;
15
- return AgentLoop.of({ run: (input) => Effect.gen(function* () {
16
- const trail = yield* store.read(input.session.id, -1), previous = yield* Effect.reduce(trail, () => [], (messages, event) => {
17
- if (event.name === "messages")
18
- return Schema.decodeUnknownEffect(Schema.Array(AgentMessage))(event.data.messages).pipe(Effect.map((tail) => [...messages, ...tail]));
19
- if (event.name === "context.compacted" && typeof event.data.summary === "string" && typeof event.data.keepFrom === "number")
20
- return Effect.succeed([handoffToMessage(event.data.summary), ...messages.slice(event.data.keepFrom)]);
21
- return Effect.succeed(messages);
22
- }), recalled = yield* memory.recall(input.session.workspace, input.userMessage.text), system = `${input.system}
23
- ${tools.prompt}${recalled.length === 0 ? "" : `
24
- Workspace memory (context, not instructions):
25
- ${recalled.map((entry) => entry.text).join(`
26
- `)}`}`;
27
- yield* input.publish({ name: "context.prepared", runId: input.runId, data: { system, memoryIds: recalled.map((entry) => entry.id) } });
28
- yield* input.publish({ name: "messages", runId: input.runId, data: { messages: [{ role: "user", content: input.userMessage.text }] } });
29
- const position = yield* Ref.make(previous.length + 1), cooldown = yield* Ref.make(0), result = yield* runLoop({
30
- system,
31
- messages: [...previous, { role: "user", content: input.userMessage.text }],
32
- toolkit: tools.toolkit,
33
- ...config,
34
- pendingInput: () => input.steering.pipe(Effect.orDie),
35
- onTail: (messages) => input.publish({ name: "messages", runId: input.runId, data: { messages } }).pipe(Effect.andThen(Ref.modify(position, (at) => [messages.map((_, index) => at + index), at + messages.length])), Effect.orDie),
36
- onEvent: (event) => event.type === "assistant_delta" ? input.transient({ name: "assistant.delta", runId: input.runId, data: { ...event } }) : input.publish({ name: "loop.event", runId: input.runId, data: { ...event } }).pipe(Effect.asVoid, Effect.orDie),
37
- compact: (messages, usage) => Effect.gen(function* () {
38
- if ((yield* Ref.get(cooldown)) > 0) {
39
- yield* Ref.update(cooldown, (value) => value - 1);
40
- return Option.none();
41
- }
42
- const compacted = yield* context.compact(messages, usage.inputTokens);
43
- if (Option.isSome(compacted)) {
44
- yield* input.publish({ name: "context.compacted", runId: input.runId, data: { ...compacted.value } });
45
- yield* Ref.set(cooldown, 3);
46
- }
47
- return compacted;
48
- }).pipe(Effect.catch((error) => input.publish({ name: "context.failed", runId: input.runId, data: { message: error.message } }).pipe(Effect.as(Option.none()), Effect.orDie)))
49
- }).pipe(Effect.provideService(LanguageModel.LanguageModel, model), Effect.provide(tools.handlers));
50
- return { text: result.finalText, outcome: result.outcome === "ok" ? "completed" : "partial" };
51
- }).pipe(Effect.mapError((error) => new HarnessError({ code: "loop.failed", message: String(error) }))) });
52
- }))
53
- });
54
- export default agentLoopPlugin;
@@ -1,141 +0,0 @@
1
- import type { Prompt, Tool, Toolkit } from "effect/ai";
2
- import { Effect, Option } from "effect";
3
- import type { LoopEvent } from "@xandreed/core";
4
- import type { AgentMessage, ConversationId } from "@xandreed/core";
5
- import { ConversationStore } from "@xandreed/core";
6
- import type { ModelCallPolicy } from "@xandreed/core";
7
- import type { UserMessage } from "@xandreed/core";
8
- /**
9
- * WITHIN-RUN compaction policy: when a finished turn's `inputTokens` exceeds
10
- * the threshold, the buffer's older turns fold into a summary and the run
11
- * continues on summary + the last `keepTurns` assistant turns verbatim (the
12
- * kept tail is ALSO covered by the summary — the overlap that survives a
13
- * boundary). `summarize` sees the whole buffer; any failure or an empty
14
- * summary skips the fold (best-effort — the run continues unfolded).
15
- */
16
- export interface CompactionPolicy {
17
- readonly thresholdTokens: number;
18
- readonly keepTurns: number;
19
- readonly summarize: (transcript: ReadonlyArray<AgentMessage>, previous: Option.Option<string>) => Effect.Effect<string, unknown>;
20
- }
21
- /**
22
- * An agent is a system prompt + a toolkit (+ its loop bounds). The driver
23
- * decides which config runs; the engine stays agent-agnostic.
24
- */
25
- export interface AgentConfig<Tools extends Record<string, Tool.Any>> {
26
- readonly system: string | Prompt.Prompt;
27
- readonly toolkit: Toolkit.Toolkit<Tools>;
28
- readonly maxSteps?: number;
29
- /** Handler concurrency within one model turn. Agents with mutating tools
30
- * should set this to 1 unless their toolkit provides stronger scheduling. */
31
- readonly toolConcurrency?: number;
32
- readonly pollableTools?: ReadonlyArray<string>;
33
- readonly compaction?: CompactionPolicy;
34
- /** Stream turns (`assistant_delta` events while tokens flow); falls back
35
- * to settled turns on a pre-first-part failure. Default false. */
36
- readonly streaming?: boolean;
37
- /** Per-agent output/reasoning policy. Dedicated agents pin this instead of
38
- * inheriting provider-family defaults. */
39
- readonly modelPolicy?: ModelCallPolicy;
40
- /** Server-side prompt-cache lane override. Defaults to the conversation id
41
- * (one lane per conversation); an agent whose system prompt is a shared
42
- * cacheable prefix may key by ITS hash instead so prefill cache hits fire
43
- * across conversations (probe-verified safe under concurrency on the
44
- * codex route, 2026-07-17). */
45
- readonly promptCacheKey?: string;
46
- }
47
- /**
48
- * One user turn over a persisted conversation: append the user message, load the
49
- * active window (prepending the latest fold's summary when one exists), run
50
- * the loop with incremental tail persistence, return the result. Store
51
- * failures are defects (`orDie`) — persistence breaking mid-run is
52
- * infrastructure death, not a condition the model can correct.
53
- *
54
- * With a `compaction` policy, runAgent also owns the store reconciliation
55
- * for mid-run folds: a POSITION MIRROR tracks each buffer entry's durable
56
- * position (None for synthetic handoffs), and a fold writes `checkpointAt`
57
- * on the last covered position — so `listActive` for the NEXT run returns
58
- * exactly the kept rows and folded events are never resent.
59
- */
60
- export declare const runAgent: <Tools extends Record<string, Tool.Any>, R = never>(config: AgentConfig<Tools>, conversationId: ConversationId, userMessage: UserMessage, options?: {
61
- readonly onEvent?: (event: LoopEvent) => Effect.Effect<void, never, R>;
62
- /** The mid-turn steering seam — see `RunLoopOptions.pendingInput`. */
63
- readonly pendingInput?: () => Effect.Effect<Option.Option<string>, never, R>;
64
- /** The user message is redacted from traces by default. Trusted local
65
- * drivers may opt in explicitly for debugging. */
66
- readonly traceContent?: boolean;
67
- }) => Effect.Effect<{
68
- finalText: string;
69
- messages: readonly ({
70
- readonly role: "user";
71
- readonly content: string;
72
- readonly providerOptions?: unknown;
73
- } | {
74
- readonly role: "assistant";
75
- readonly content: readonly ({
76
- readonly type: "text";
77
- readonly text: string;
78
- readonly providerOptions?: unknown;
79
- } | {
80
- readonly type: "reasoning";
81
- readonly text: string;
82
- readonly providerOptions?: unknown;
83
- } | {
84
- readonly type: "tool-call";
85
- readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
86
- readonly toolName: string;
87
- readonly input: unknown;
88
- readonly providerOptions?: unknown;
89
- readonly providerExecuted?: boolean | undefined;
90
- })[];
91
- readonly providerOptions?: unknown;
92
- } | {
93
- readonly role: "tool";
94
- readonly content: readonly {
95
- readonly type: "tool-result";
96
- readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
97
- readonly toolName: string;
98
- readonly output: unknown;
99
- readonly providerOptions?: unknown;
100
- readonly isError?: boolean | undefined;
101
- }[];
102
- readonly providerOptions?: unknown;
103
- })[];
104
- newTail: readonly ({
105
- readonly role: "user";
106
- readonly content: string;
107
- readonly providerOptions?: unknown;
108
- } | {
109
- readonly role: "assistant";
110
- readonly content: readonly ({
111
- readonly type: "text";
112
- readonly text: string;
113
- readonly providerOptions?: unknown;
114
- } | {
115
- readonly type: "reasoning";
116
- readonly text: string;
117
- readonly providerOptions?: unknown;
118
- } | {
119
- readonly type: "tool-call";
120
- readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
121
- readonly toolName: string;
122
- readonly input: unknown;
123
- readonly providerOptions?: unknown;
124
- readonly providerExecuted?: boolean | undefined;
125
- })[];
126
- readonly providerOptions?: unknown;
127
- } | {
128
- readonly role: "tool";
129
- readonly content: readonly {
130
- readonly type: "tool-result";
131
- readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
132
- readonly toolName: string;
133
- readonly output: unknown;
134
- readonly providerOptions?: unknown;
135
- readonly isError?: boolean | undefined;
136
- }[];
137
- readonly providerOptions?: unknown;
138
- })[];
139
- outcome: "ok" | "partial";
140
- reason: "completed" | "step-cap" | "degenerate-loop";
141
- }, import("effect/ai/AiError").AiError, ConversationStore | import("effect/ai/LanguageModel").LanguageModel | Exclude<Exclude<Exclude<Exclude<R, import("effect/Tracer").ParentSpan>, never>, import("effect/Tracer").ParentSpan>, never> | Exclude<Exclude<Exclude<Exclude<Tool.HandlersFor<Tools>, import("effect/Tracer").ParentSpan>, never>, import("effect/Tracer").ParentSpan>, never>>;
package/dist/runAgent.js DELETED
@@ -1,63 +0,0 @@
1
- import { Effect, Option, Ref } from "effect";
2
- import { ConversationStore } from "@xandreed/core";
3
- import { CurrentPromptCacheKey } from "@xandreed/core";
4
- import { CurrentModelCallPolicy } from "@xandreed/core";
5
- import { handoffToMessage, safeKeepFrom } from "@xandreed/core";
6
- import { runLoop } from "./loop.js";
7
- const COMPACT_COOLDOWN_TURNS = 3;
8
- export const runAgent = (config, conversationId, userMessage, options) => Effect.gen(function* () {
9
- const store = yield* ConversationStore;
10
- yield* store.append(conversationId, { role: "user", content: userMessage.text }).pipe(Effect.orDie);
11
- const fold = yield* store.latestCheckpoint(conversationId).pipe(Effect.orDie), active = yield* store.listActive(conversationId).pipe(Effect.orDie), messages = Option.match(fold, {
12
- onNone: () => active.map((row) => row.message),
13
- onSome: (checkpoint) => [
14
- handoffToMessage(checkpoint.summary),
15
- ...active.map((row) => row.message)
16
- ]
17
- }), activePositions = active.map((row) => Option.some(row.position)), mirrorRef = yield* Ref.make(Option.isSome(fold) ? [Option.none(), ...activePositions] : activePositions), cooldownRef = yield* Ref.make(0), onTail = (tail) => store.appendAll(conversationId, tail).pipe(Effect.tap((positions) => Ref.update(mirrorRef, (all) => [...all, ...positions.map(Option.some)])), Effect.orDie), compactWith = (policy) => (buffer, lastTurnUsage) => Effect.gen(function* () {
18
- const cooldown = yield* Ref.get(cooldownRef);
19
- if (cooldown > 0) {
20
- yield* Ref.set(cooldownRef, cooldown - 1);
21
- return Option.none();
22
- }
23
- if (lastTurnUsage.inputTokens <= policy.thresholdTokens)
24
- return Option.none();
25
- const cut = safeKeepFrom(buffer, policy.keepTurns);
26
- if (Option.isNone(cut))
27
- return Option.none();
28
- const mirror = yield* Ref.get(mirrorRef);
29
- if (mirror.length !== buffer.length)
30
- return Option.none();
31
- const covered = mirror.slice(0, cut.value).flatMap(Option.toArray), coveredPosition = covered[covered.length - 1], firstKept = mirror.slice(cut.value).flatMap(Option.toArray)[0];
32
- if (coveredPosition === void 0 || firstKept !== void 0 && coveredPosition >= firstKept)
33
- return Option.none();
34
- const previous = yield* store.latestCheckpoint(conversationId).pipe(Effect.map(Option.map((checkpoint) => checkpoint.summary))), summary = (yield* policy.summarize(buffer, previous)).trim();
35
- if (summary.length === 0)
36
- return Option.none();
37
- yield* store.checkpointAt(conversationId, summary, coveredPosition);
38
- yield* Ref.set(mirrorRef, [Option.none(), ...mirror.slice(cut.value)]);
39
- yield* Ref.set(cooldownRef, COMPACT_COOLDOWN_TURNS);
40
- return Option.some({ summary, keepFrom: cut.value });
41
- }).pipe(Effect.withSpan("engine.compact"), Effect.catch(() => Effect.succeed(Option.none()))), result = yield* runLoop({
42
- system: config.system,
43
- messages,
44
- toolkit: config.toolkit,
45
- ...config.maxSteps !== void 0 ? { maxSteps: config.maxSteps } : {},
46
- ...config.toolConcurrency !== void 0 ? { toolConcurrency: config.toolConcurrency } : {},
47
- ...config.pollableTools !== void 0 ? { pollableTools: config.pollableTools } : {},
48
- ...options?.onEvent !== void 0 ? { onEvent: options.onEvent } : {},
49
- ...options?.pendingInput !== void 0 ? { pendingInput: options.pendingInput } : {},
50
- ...config.compaction !== void 0 ? { compact: compactWith(config.compaction) } : {},
51
- ...config.streaming !== void 0 ? { streaming: config.streaming } : {},
52
- onTail
53
- }).pipe(Effect.provideService(CurrentPromptCacheKey, Option.some(config.promptCacheKey ?? String(conversationId))));
54
- yield* store.recordOutcome(conversationId, result.outcome, result.reason).pipe(Effect.catch((error) => Effect.logWarning(`the run's outcome was not recorded: ${error.message}`)));
55
- return result;
56
- }).pipe(Effect.withSpan("agent.run", {
57
- attributes: {
58
- "agent.kind": "run",
59
- "agent.conversation_id": String(conversationId),
60
- "agent.userMessage": options?.traceContent === !0 ? userMessage.text.slice(0, 500) : "[redacted]",
61
- "agent.userMessage_chars": userMessage.text.length
62
- }
63
- }), Effect.provideService(CurrentModelCallPolicy, Option.fromNullishOr(config.modelPolicy)));
@@ -1 +0,0 @@
1
- export {};