@xandreed/plugin-agent-loop 0.3.0 → 0.4.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.3.0. Bun on Linux.
3
+ Efferent composable agent framework, 0.4.0. Bun on Linux.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
package/dist/loop.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LanguageModel, Prompt } from "@effect/ai";
1
+ import { AiError, LanguageModel, Prompt } from "@effect/ai";
2
2
  import type { Tool, Toolkit } from "@effect/ai";
3
3
  import { Effect, Option } from "effect";
4
4
  import type { LoopEvent } from "@xandreed/core";
@@ -26,6 +26,23 @@ export interface CompactionPlan {
26
26
  * `Effect.iterate` — the same discipline as foundry's forge loop.
27
27
  */
28
28
  export interface RunLoopOptions<Tools extends Record<string, Tool.Any>, R> {
29
+ /** A host-validated first batch. Runs as step zero through the ordinary
30
+ * Effect AI decoder, instrumented toolkit and durable tail hooks, without
31
+ * another provider request. Provider usage belongs to the host planner. */
32
+ readonly initialStep?: ReadonlyArray<{
33
+ readonly name: keyof Tools & string;
34
+ readonly params: unknown;
35
+ }>;
36
+ /** Host selection at a real provider boundary. Never called for initialStep.
37
+ * The host may cache its choice for a run; tools and completion remain loop-owned. */
38
+ readonly prepareModel?: (input: {
39
+ readonly stepIndex: number;
40
+ readonly messages: ReadonlyArray<AgentMessage>;
41
+ readonly activeTools: ReadonlyArray<string>;
42
+ }) => Effect.Effect<{
43
+ readonly model: LanguageModel.Service;
44
+ readonly system: Prompt.Prompt;
45
+ }, AiError.AiError, R>;
29
46
  readonly system: string | Prompt.Prompt;
30
47
  /** Explicit opt-in: export prompt and result content on step spans.
31
48
  * Hosts own consent, redaction and exporter retention. Disabled by default. */
@@ -190,4 +207,4 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
190
207
  })[];
191
208
  outcome: "ok" | "partial";
192
209
  reason: "completed" | "step-cap" | "degenerate-loop";
193
- }, import("@effect/ai/AiError").AiError, LanguageModel.LanguageModel | Exclude<R, import("effect/Tracer").ParentSpan> | Exclude<Tool.HandlersFor<Tools>, import("effect/Tracer").ParentSpan>>;
210
+ }, AiError.AiError, LanguageModel.LanguageModel | Exclude<R, import("effect/Tracer").ParentSpan> | Exclude<Tool.HandlersFor<Tools>, import("effect/Tracer").ParentSpan>>;
package/dist/loop.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LanguageModel, Prompt } from "@effect/ai";
1
+ import { AiError, LanguageModel, Prompt } from "@effect/ai";
2
2
  import { Cause, Effect, Exit, Match, Metric, Option, Ref, Stream } from "effect";
3
3
  import { foldStreamParts } from "@xandreed/core";
4
4
  import { CurrentAgentStep } from "@xandreed/core";
@@ -42,7 +42,7 @@ export const foldProgress = (state, sig) => {
42
42
  yield* onEvent({ type: "turn_start", turnIndex: state.turnIndex });
43
43
  const active = options.activeTools === void 0 ? Object.keys(toolkit.tools) : yield* options.activeTools().pipe(Effect.provide(eventContext)), selectedTools = Object.fromEntries(Object.entries(toolkit.tools).filter(([name]) => active.includes(name))), toolNames = Object.keys(selectedTools);
44
44
  yield* Effect.annotateCurrentSpan({ "engine.tools.active": toolNames });
45
- const instructions = typeof options.system === "string" ? Prompt.make([{ role: "system", content: options.system }]) : options.system, prompt = Prompt.merge(instructions, Prompt.make(toPromptMessages(state.messages)));
45
+ const prepared = state.turnIndex === 0 && options.initialStep !== void 0 ? Option.none() : options.prepareModel === void 0 ? Option.none() : Option.some(yield* options.prepareModel({ stepIndex: state.turnIndex, messages: state.messages, activeTools: toolNames }).pipe(Effect.provide(eventContext))), baseSystem = Option.match(prepared, { onNone: () => options.system, onSome: (value) => value.system }), instructions = typeof baseSystem === "string" ? Prompt.make([{ role: "system", content: baseSystem }]) : baseSystem, prompt = Prompt.merge(instructions, Prompt.make(toPromptMessages(state.messages)));
46
46
  if (options.captureTraceContent === !0)
47
47
  yield* Effect.annotateCurrentSpan({
48
48
  "engine.step.input": JSON.stringify({ messages: prompt.content, activeTools: toolNames })
@@ -66,14 +66,14 @@ export const foldProgress = (state, sig) => {
66
66
  })
67
67
  },
68
68
  concurrency: options.toolConcurrency ?? DEFAULT_TOOL_CONCURRENCY
69
- }, settled = (streamingHealthy) => LanguageModel.generateText(callOptions).pipe(Effect.map((res) => ({
69
+ }, settled = (streamingHealthy) => (Option.isSome(prepared) ? prepared.value.model.generateText(callOptions) : LanguageModel.generateText(callOptions)).pipe(Effect.map((res) => ({
70
70
  _tag: "ok",
71
71
  content: res.content,
72
72
  finishReason: res.finishReason,
73
73
  usage: res.usage,
74
74
  streamingHealthy
75
75
  }))), streamed = Effect.gen(function* () {
76
- const partSeen = yield* Ref.make(!1), folded = yield* foldStreamParts(LanguageModel.streamText(callOptions).pipe(Stream.tap(() => Ref.set(partSeen, !0))), (delta) => onEvent({
76
+ const partSeen = yield* Ref.make(!1), folded = yield* foldStreamParts((Option.isSome(prepared) ? prepared.value.model.streamText(callOptions) : LanguageModel.streamText(callOptions)).pipe(Stream.tap(() => Ref.set(partSeen, !0))), (delta) => onEvent({
77
77
  type: "assistant_delta",
78
78
  turnIndex: state.turnIndex,
79
79
  channel: delta.channel,
@@ -91,7 +91,20 @@ export const foldProgress = (state, sig) => {
91
91
  onSome: Effect.succeed,
92
92
  onNone: () => Effect.logWarning("streaming failed before any part arrived \u2014 falling back to generateText for this run").pipe(Effect.zipRight(settled(!1)))
93
93
  });
94
- }), outcome = yield* (options.streaming === !0 && state.streamingHealthy ? streamed : settled(state.streamingHealthy)).pipe(Effect.catchAll((err) => err?._tag === "MalformedOutput" ? Effect.succeed({ _tag: "malformed", err }) : Effect.fail(err)), Effect.locally(CurrentEmptyResponseTolerance, state.toolCalls > 0));
94
+ }), planned = state.turnIndex === 0 && options.initialStep !== void 0, initial = Effect.gen(function* () {
95
+ const provider = yield* LanguageModel.make({
96
+ generateText: () => Effect.succeed([...(options.initialStep ?? []).map((call, index) => ({
97
+ type: "tool-call",
98
+ id: `planned:0:${index}`,
99
+ name: call.name,
100
+ params: call.params,
101
+ providerExecuted: !1
102
+ })), { type: "finish", reason: "tool-calls", usage: { inputTokens: void 0, outputTokens: void 0, totalTokens: void 0 } }]),
103
+ streamText: () => Stream.empty
104
+ });
105
+ yield* Effect.annotateCurrentSpan("engine.step.host_planned", !0);
106
+ return yield* settled(state.streamingHealthy).pipe(Effect.provideService(LanguageModel.LanguageModel, provider));
107
+ }), useStreaming = options.streaming === !0 && state.streamingHealthy, outcome = yield* (planned ? initial : useStreaming ? streamed : settled(state.streamingHealthy)).pipe(Effect.catchAll((err) => err?._tag === "MalformedOutput" ? Effect.succeed({ _tag: "malformed", err }) : Effect.fail(err)), Effect.locally(CurrentEmptyResponseTolerance, state.toolCalls > 0));
95
108
  if (outcome._tag === "malformed") {
96
109
  const streak = state.malformedStreak + 1;
97
110
  if (streak >= MAX_MALFORMED)
@@ -121,10 +134,11 @@ This usually means you called a tool that doesn't exist or used the wrong argume
121
134
  const content = withToolCallIds(outcome.content, state.turnIndex), usage = extractUsage(outcome.usage, content);
122
135
  yield* Effect.annotateCurrentSpan({
123
136
  "engine.step.finish_reason": outcome.finishReason,
124
- "engine.step.usage": JSON.stringify(usage),
137
+ "engine.step.usage_available": !planned,
138
+ ...!planned ? { "engine.step.usage": JSON.stringify(usage) } : {},
125
139
  ...options.captureTraceContent === !0 ? { "engine.step.output": JSON.stringify({ content, usage, finishReason: outcome.finishReason }) } : {}
126
140
  });
127
- const model = extractModel(content), tail = withUsageOnAssistant(responseToAgentMessages(content), usage, model), positions = yield* options.onTail?.(tail) ?? Effect.succeed([]), assistantAt = tail.findIndex((m) => m.role === "assistant"), assistantPosition = assistantAt >= 0 ? Option.fromNullable(positions[assistantAt]) : Option.none(), text = responseText(content), toolCalls = responseToolCalls(content);
141
+ const model = extractModel(content), tail = planned ? responseToAgentMessages(content) : withUsageOnAssistant(responseToAgentMessages(content), usage, model), positions = yield* options.onTail?.(tail) ?? Effect.succeed([]), assistantAt = tail.findIndex((m) => m.role === "assistant"), assistantPosition = assistantAt >= 0 ? Option.fromNullable(positions[assistantAt]) : Option.none(), text = responseText(content), toolCalls = responseToolCalls(content);
128
142
  yield* onEvent({
129
143
  type: "assistant_message",
130
144
  turnIndex: state.turnIndex,
@@ -5,7 +5,7 @@ import { runLoop } from "./loop.js";
5
5
  const Config = Schema.Struct({ maxSteps: Schema.Int.pipe(Schema.between(1, 1000)), toolConcurrency: Schema.Int.pipe(Schema.between(1, 32)), streaming: Schema.Boolean });
6
6
  export const agentLoopPlugin = definePlugin({
7
7
  id: "@xandreed/plugin-agent-loop",
8
- version: "0.3.0",
8
+ version: "0.4.0",
9
9
  config: Config,
10
10
  defaults: { maxSteps: 100, toolConcurrency: 1, streaming: !0 },
11
11
  requires: [LanguageModel.LanguageModel, AgentTools, SessionStore, Memory, ContextManager],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xandreed/plugin-agent-loop",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -10,7 +10,7 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
- "@xandreed/core": "0.3.0",
13
+ "@xandreed/core": "0.4.0",
14
14
  "@effect/ai": "0.35.0",
15
15
  "effect": "3.21.4"
16
16
  },