@dudko.dev/agent-web 0.0.6 → 0.0.7

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/dist/index.cjs CHANGED
@@ -119,6 +119,7 @@ __export(src_exports, {
119
119
  preloadWebLLMModel: () => preloadWebLLMModel,
120
120
  promptHintOf: () => promptHintOf,
121
121
  renderCatalog: () => renderCatalog,
122
+ replanWanted: () => replanWanted,
122
123
  resolveConfig: () => resolveConfig,
123
124
  resolveModel: () => resolveModel,
124
125
  resolveStage: () => resolveStage,
@@ -153,6 +154,7 @@ var resolveConfig = (c) => ({
153
154
  },
154
155
  temperature: c.temperature,
155
156
  replan: c.replan ?? true,
157
+ replanAfter: c.replanAfter ?? "failure",
156
158
  synthesize: c.synthesize ?? true,
157
159
  compressAfterChars: c.compressAfterChars ?? 12e3,
158
160
  logLevel: c.logLevel ?? "warn"
@@ -162,6 +164,7 @@ var resolveConfig = (c) => ({
162
164
  var import_ai = require("ai");
163
165
 
164
166
  // src/llm/util.ts
167
+ var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
165
168
  var promptOf = (opts) => opts.messages ? { messages: opts.messages } : { prompt: opts.prompt ?? "" };
166
169
  var normalizeUsage = (u) => {
167
170
  const inputTokens = u?.inputTokens ?? 0;
@@ -434,7 +437,6 @@ var stateBlock = (state) => state && state.trim() ? `
434
437
 
435
438
  STATE:
436
439
  ${state}` : "";
437
- var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
438
440
  var historyBlock = (history) => {
439
441
  if (!history || history.length === 0) return "";
440
442
  const lines = history.slice(-8).map((m) => `${m.role}: ${clip(m.content, 400)}`);
@@ -467,8 +469,9 @@ Reply with a single JSON object, nothing else:
467
469
  - "actions" are the tool calls for THIS step ([] if none are needed). Use ONLY tools from the TOOLS list; "args" must match the tool's parameters.
468
470
  - Build on the current STATE \u2014 do not repeat work that is already there.
469
471
  - "reply" is one short human sentence (no JSON) describing what you did.
472
+ - After your actions run you will see their TOOL RESULTS and may continue the same step; finish with "actions": [] once it is done.
470
473
  - If you CANNOT complete the step, set "actions": [] and put the token [BLOCKER] in "reply" with a short reason.`;
471
- var REPLANNER_NATIVE = `You are the REPLANNER. After a step that was blocked or had a failed tool call, decide whether to keep going, revise the remaining steps, or finish.
474
+ var REPLANNER_NATIVE = `You are the REPLANNER. After an executed step, decide whether to keep going, revise the remaining steps, or finish.
472
475
  "continue": the remaining steps still fit. "revise": provide a better "plan" for the REMAINING work (never repeat done work). "finish": the goal is already met.
473
476
  Judge from the current STATE vs the goal. Prefer "continue".`;
474
477
  var REPLANNER_PROMPTED = `You are the REPLANNER. After each executed step you decide whether to keep going, revise the remaining steps, or finish.
@@ -598,6 +601,15 @@ var dispatch = async (action, tools, ctx = {}) => {
598
601
  // src/llm/tool-loop.ts
599
602
  var import_ai3 = require("ai");
600
603
 
604
+ // src/agent/loop-types.ts
605
+ var BLOCKER = "[BLOCKER]";
606
+ var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
607
+ var addUsage = (a, b) => ({
608
+ inputTokens: a.inputTokens + b.inputTokens,
609
+ outputTokens: a.outputTokens + b.outputTokens,
610
+ totalTokens: a.totalTokens + b.totalTokens
611
+ });
612
+
601
613
  // src/parse.ts
602
614
  var stripFences = (text) => (text || "").replace(/```(?:json)?/gi, "").trim();
603
615
  var extractBalanced = (text, open) => {
@@ -796,34 +808,74 @@ var runNative = async (model, opts) => {
796
808
  const [text, usage] = await Promise.all([result.text, result.usage]);
797
809
  return { text: text.trim(), toolCalls, usage: normalizeUsage(usage) };
798
810
  };
811
+ var asJson = (value) => {
812
+ try {
813
+ return JSON.stringify(value) ?? String(value);
814
+ } catch {
815
+ return String(value);
816
+ }
817
+ };
818
+ var toolResultsPrompt = (results) => {
819
+ const lines = results.map(
820
+ (r) => `- ${r.name} ${clip(asJson(r.input), 160)} \u2192 ${r.ok ? "ok" : "FAILED"}: ${clip(asJson(r.output), 400)}`
821
+ );
822
+ return `TOOL RESULTS:
823
+ ${lines.join("\n")}
824
+
825
+ Continue THIS step using the results above. Reply with a single JSON object, nothing else:
826
+ { "reply": string, "actions": [ { "tool": string, "args": object } ] }
827
+ - If the step is now complete: "actions": [] and a short outcome in "reply".
828
+ - Otherwise emit ONLY the remaining or corrective calls \u2014 never repeat a successful call.`;
829
+ };
799
830
  var runPrompted = async (model, opts) => {
800
- const result = await (0, import_ai3.generateText)({
801
- model,
802
- system: opts.system,
803
- ...promptOf(opts),
804
- maxOutputTokens: opts.maxOutputTokens,
805
- temperature: opts.temperature,
806
- abortSignal: timeoutSignal(opts.abortSignal, opts.timeoutMs)
807
- });
808
- const parsed = parseExecutorResponse(result.text);
809
- if (parsed.reply) opts.callbacks?.onTextDelta?.(parsed.reply);
831
+ const maxRounds = Math.max(1, opts.maxSteps ?? 4);
810
832
  const active = opts.activeTools;
811
833
  const tools = active ? Object.fromEntries(Object.entries(opts.tools).filter(([name]) => active.includes(name))) : opts.tools;
834
+ const signal = timeoutSignal(opts.abortSignal, opts.timeoutMs);
835
+ const messages = opts.messages ? [...opts.messages] : [{ role: "user", content: opts.prompt ?? "" }];
812
836
  const toolCalls = [];
813
- for (const action of parsed.actions) {
814
- opts.callbacks?.onToolCall?.(action.tool, action.args);
815
- const rec = await dispatch(action, tools, { abortSignal: opts.abortSignal });
816
- toolCalls.push(rec);
817
- opts.callbacks?.onToolResult?.(rec.name, rec.output, rec.ok);
837
+ const seen = /* @__PURE__ */ new Set();
838
+ let usage = emptyUsage();
839
+ let text = "";
840
+ for (let round = 1; round <= maxRounds; round += 1) {
841
+ if (signal?.aborted) break;
842
+ const result = await (0, import_ai3.generateText)({
843
+ model,
844
+ system: opts.system,
845
+ messages,
846
+ maxOutputTokens: opts.maxOutputTokens,
847
+ temperature: opts.temperature,
848
+ abortSignal: signal
849
+ });
850
+ usage = addUsage(usage, normalizeUsage(result.usage));
851
+ const parsed = parseExecutorResponse(result.text);
852
+ text = parsed.reply;
853
+ if (parsed.reply.includes(BLOCKER)) break;
854
+ if (parsed.actions.length === 0) break;
855
+ const batch = asJson(parsed.actions);
856
+ if (seen.has(batch)) break;
857
+ seen.add(batch);
858
+ const results = [];
859
+ for (const action of parsed.actions) {
860
+ if (signal?.aborted) break;
861
+ opts.callbacks?.onToolCall?.(action.tool, action.args);
862
+ const rec = await dispatch(action, tools, { abortSignal: signal });
863
+ toolCalls.push(rec);
864
+ results.push(rec);
865
+ opts.callbacks?.onToolResult?.(rec.name, rec.output, rec.ok);
866
+ }
867
+ if (round === maxRounds || signal?.aborted) break;
868
+ messages.push({ role: "assistant", content: result.text });
869
+ messages.push({ role: "user", content: toolResultsPrompt(results) });
818
870
  }
819
- return { text: parsed.reply, toolCalls, usage: normalizeUsage(result.usage) };
871
+ if (text) opts.callbacks?.onTextDelta?.(text);
872
+ return { text, toolCalls, usage };
820
873
  };
821
874
 
822
875
  // src/agent/internal.ts
823
876
  var systemFor = (ctx, base) => withSystem(base, ctx.raw.systemPrompt);
824
877
 
825
878
  // src/agent/executor.ts
826
- var BLOCKER = "[BLOCKER]";
827
879
  var splitBlocker = (raw) => {
828
880
  if (!raw.includes(BLOCKER)) return { summary: raw.trim(), blocked: false };
829
881
  return { summary: raw.split(BLOCKER).join("").trim(), blocked: true };
@@ -888,15 +940,35 @@ var executeStep = async (ctx, goal, step, index, total, done) => {
888
940
  usage: loop.usage
889
941
  };
890
942
  };
891
- var shouldReplan = (result) => result.blocked || result.toolCalls.some((c) => !c.ok);
892
-
893
- // src/agent/loop-types.ts
894
- var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
895
- var addUsage = (a, b) => ({
896
- inputTokens: a.inputTokens + b.inputTokens,
897
- outputTokens: a.outputTokens + b.outputTokens,
898
- totalTokens: a.totalTokens + b.totalTokens
899
- });
943
+ var shouldReplan = (result) => {
944
+ if (result.blocked) return true;
945
+ const { toolCalls } = result;
946
+ return toolCalls.some(
947
+ (c, i) => !c.ok && !toolCalls.slice(i + 1).some((later) => later.ok && later.name === c.name)
948
+ );
949
+ };
950
+ var replanWanted = async (trigger, result, opts = {}) => {
951
+ if (trigger === "always") return true;
952
+ if (typeof trigger !== "function") return shouldReplan(result);
953
+ const fallback = () => shouldReplan(result);
954
+ const decided = (async () => {
955
+ try {
956
+ return Boolean(await trigger(result));
957
+ } catch (err) {
958
+ opts.onError?.(err);
959
+ return fallback();
960
+ }
961
+ })();
962
+ const guard = timeoutSignal(opts.signal, opts.timeoutMs);
963
+ if (!guard) return decided;
964
+ return Promise.race([
965
+ decided,
966
+ new Promise((resolve) => {
967
+ if (guard.aborted) resolve(fallback());
968
+ else guard.addEventListener("abort", () => resolve(fallback()), { once: true });
969
+ })
970
+ ]);
971
+ };
900
972
 
901
973
  // src/agent/schemas.ts
902
974
  var import_zod = require("zod");
@@ -1249,7 +1321,12 @@ var createAgent = async (config) => {
1249
1321
  done.push(
1250
1322
  `${step.description} \u2014 ${applied} applied${failed ? `, ${failed} failed` : ""}${stepResult.blocked ? ", blocked" : ""}`
1251
1323
  );
1252
- if (cfg.replan && iter < cfg.maxIterations && revisions < cfg.maxRevisions && !isAborted() && shouldReplan(stepResult)) {
1324
+ const wantReplan = cfg.replan && iter < cfg.maxIterations && revisions < cfg.maxRevisions && !isAborted() && await replanWanted(cfg.replanAfter, stepResult, {
1325
+ signal: ctx.signal,
1326
+ timeoutMs: cfg.chatTimeoutMs,
1327
+ onError: (err) => log.warn("replanAfter predicate threw \u2014 using the failure rule", err)
1328
+ });
1329
+ if (wantReplan && !isAborted()) {
1253
1330
  const decision = await decideReplan(
1254
1331
  ctx,
1255
1332
  text,
@@ -1578,6 +1655,7 @@ var IndexedDBStore = class {
1578
1655
  preloadWebLLMModel,
1579
1656
  promptHintOf,
1580
1657
  renderCatalog,
1658
+ replanWanted,
1581
1659
  resolveConfig,
1582
1660
  resolveModel,
1583
1661
  resolveStage,