@synergenius/flow-weaver 0.24.1 → 0.24.3

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.
@@ -79,6 +79,17 @@ export async function runAgentLoop(provider, tools, executor, messages, options)
79
79
  }
80
80
  // If no tool calls, we're done
81
81
  if (finishReason !== 'tool_calls' || collectedToolCalls.length === 0) {
82
+ // Final turn hook
83
+ if (options?.onTurnEnd) {
84
+ await options.onTurnEnd({
85
+ iteration,
86
+ maxIterations,
87
+ messages: conversation,
88
+ toolCallCount,
89
+ usage: { promptTokens: totalPromptTokens, completionTokens: totalCompletionTokens },
90
+ isFinalTurn: true,
91
+ });
92
+ }
82
93
  return buildResult(finishReason !== 'error', text || 'Task completed', conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
83
94
  }
84
95
  // Execute tool calls and add results to conversation
@@ -106,6 +117,23 @@ export async function runAgentLoop(provider, tools, executor, messages, options)
106
117
  toolCallId: tc.id,
107
118
  });
108
119
  }
120
+ // Between-turns hook — runs after tool execution, before next LLM call
121
+ if (options?.onTurnEnd) {
122
+ const turnResult = await options.onTurnEnd({
123
+ iteration,
124
+ maxIterations,
125
+ messages: conversation,
126
+ toolCallCount,
127
+ usage: { promptTokens: totalPromptTokens, completionTokens: totalCompletionTokens },
128
+ isFinalTurn: false,
129
+ });
130
+ if (turnResult?.continue === false) {
131
+ return buildResult(true, turnResult.injectMessage ?? 'Stopped by hook', conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
132
+ }
133
+ if (turnResult?.injectMessage) {
134
+ conversation.push({ role: 'user', content: turnResult.injectMessage });
135
+ }
136
+ }
109
137
  }
110
138
  return buildResult(false, `Reached max iterations (${maxIterations})`, conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
111
139
  }
@@ -4,7 +4,7 @@
4
4
  * Provider-agnostic agent loop with MCP bridge for tool execution.
5
5
  * Built-in providers: Anthropic API, Claude CLI, OpenAI-compatible (GPT-4o, Groq, Ollama, etc).
6
6
  */
7
- export type { SplitPrompt, StreamEvent, AgentMessage, AgentProvider, ToolDefinition, ToolExecutor, ToolEvent, McpBridge, AgentLoopOptions, AgentLoopResult, StreamOptions, SpawnFn, ClaudeCliProviderOptions, CliSessionOptions, Logger, } from './types.js';
7
+ export type { SplitPrompt, TurnEndContext, TurnEndResult, StreamEvent, AgentMessage, AgentProvider, ToolDefinition, ToolExecutor, ToolEvent, McpBridge, AgentLoopOptions, AgentLoopResult, StreamOptions, SpawnFn, ClaudeCliProviderOptions, CliSessionOptions, Logger, } from './types.js';
8
8
  export { joinSplitPrompt } from './types.js';
9
9
  export { runAgentLoop } from './agent-loop.js';
10
10
  export { AnthropicProvider, createAnthropicProvider } from './providers/anthropic.js';
@@ -105,6 +105,31 @@ export interface McpBridge {
105
105
  /** Tear down the socket server and remove temp files */
106
106
  cleanup: () => void;
107
107
  }
108
+ /** Context passed to the onTurnEnd callback after each agent loop iteration. */
109
+ export interface TurnEndContext {
110
+ /** Current iteration number (0-based). */
111
+ iteration: number;
112
+ /** Maximum iterations configured. */
113
+ maxIterations: number;
114
+ /** Full conversation history up to this point. */
115
+ messages: ReadonlyArray<AgentMessage>;
116
+ /** Total tool calls executed so far. */
117
+ toolCallCount: number;
118
+ /** Cumulative token usage. */
119
+ usage: {
120
+ promptTokens: number;
121
+ completionTokens: number;
122
+ };
123
+ /** true if the model stopped calling tools (final turn). */
124
+ isFinalTurn: boolean;
125
+ }
126
+ /** Result from the onTurnEnd callback. */
127
+ export interface TurnEndResult {
128
+ /** If false, abort the agent loop early. */
129
+ continue?: boolean;
130
+ /** Optional message to inject into the conversation (steering nudge). */
131
+ injectMessage?: string;
132
+ }
108
133
  export interface AgentLoopOptions {
109
134
  systemPrompt?: SplitPrompt;
110
135
  maxIterations?: number;
@@ -113,6 +138,8 @@ export interface AgentLoopOptions {
113
138
  signal?: AbortSignal;
114
139
  onToolEvent?: (event: ToolEvent) => void;
115
140
  onStreamEvent?: (event: StreamEvent) => void;
141
+ /** Called after each agent loop iteration (between turns and on final turn). */
142
+ onTurnEnd?: (context: TurnEndContext) => Promise<TurnEndResult | void>;
116
143
  logger?: Logger;
117
144
  }
118
145
  export interface AgentLoopResult {
@@ -9886,7 +9886,7 @@ var VERSION;
9886
9886
  var init_generated_version = __esm({
9887
9887
  "src/generated-version.ts"() {
9888
9888
  "use strict";
9889
- VERSION = "0.24.1";
9889
+ VERSION = "0.24.3";
9890
9890
  }
9891
9891
  });
9892
9892
 
@@ -36053,8 +36053,10 @@ var init_parser2 = __esm({
36053
36053
  }
36054
36054
  try {
36055
36055
  let nodeTypes;
36056
- if (this.importCache.has(importedFilePath)) {
36057
- nodeTypes = this.importCache.get(importedFilePath);
36056
+ const importStats = fs5.statSync(importedFilePath);
36057
+ const cached2 = this.importCache.get(importedFilePath);
36058
+ if (cached2 && cached2.mtime === importStats.mtimeMs) {
36059
+ nodeTypes = cached2.nodeTypes;
36058
36060
  } else {
36059
36061
  this.importStack.add(importedFilePath);
36060
36062
  try {
@@ -36078,7 +36080,7 @@ var init_parser2 = __esm({
36078
36080
  const inferredFromImport = this.inferAllUnannotatedFunctions(importedFile, nodeTypes);
36079
36081
  nodeTypes.push(...inferredFromImport);
36080
36082
  this.project.removeSourceFile(importedFile);
36081
- this.importCache.set(importedFilePath, nodeTypes);
36083
+ this.importCache.set(importedFilePath, { mtime: importStats.mtimeMs, nodeTypes });
36082
36084
  } finally {
36083
36085
  this.importStack.delete(importedFilePath);
36084
36086
  }
@@ -36119,8 +36121,21 @@ var init_parser2 = __esm({
36119
36121
  const importedNames = /* @__PURE__ */ new Set();
36120
36122
  namedImports.forEach((ni) => importedNames.add(ni.getName()));
36121
36123
  const cacheKey = `npm:${moduleSpecifier}`;
36122
- if (this.importCache.has(cacheKey)) {
36123
- return this.importCache.get(cacheKey).filter((nt) => importedNames.has(nt.functionName));
36124
+ const npmCached = this.importCache.get(cacheKey);
36125
+ if (npmCached) {
36126
+ const currentDir2 = path6.dirname(currentFilePath);
36127
+ const resolvedDts = resolvePackageTypesPath(moduleSpecifier, currentDir2);
36128
+ if (resolvedDts) {
36129
+ try {
36130
+ const dtsStats = fs5.statSync(resolvedDts);
36131
+ if (npmCached.mtime === dtsStats.mtimeMs) {
36132
+ return npmCached.nodeTypes.filter((nt) => importedNames.has(nt.functionName));
36133
+ }
36134
+ } catch {
36135
+ }
36136
+ } else {
36137
+ return npmCached.nodeTypes.filter((nt) => importedNames.has(nt.functionName));
36138
+ }
36124
36139
  }
36125
36140
  const currentDir = path6.dirname(currentFilePath);
36126
36141
  const dtsPath = resolvePackageTypesPath(moduleSpecifier, currentDir);
@@ -36146,7 +36161,8 @@ var init_parser2 = __esm({
36146
36161
  allNodeTypes.push(nodeType);
36147
36162
  }
36148
36163
  this.project.removeSourceFile(dtsFile);
36149
- this.importCache.set(cacheKey, allNodeTypes);
36164
+ const dtsMtime = fs5.statSync(dtsPath).mtimeMs;
36165
+ this.importCache.set(cacheKey, { mtime: dtsMtime, nodeTypes: allNodeTypes });
36150
36166
  return allNodeTypes.filter((nt) => importedNames.has(nt.functionName));
36151
36167
  } catch {
36152
36168
  return [];
@@ -36215,7 +36231,7 @@ var init_parser2 = __esm({
36215
36231
  const cacheKey = `npm:${imp.importSource}`;
36216
36232
  if (this.importCache.has(cacheKey)) {
36217
36233
  const cached2 = this.importCache.get(cacheKey);
36218
- const found = cached2.find((nt) => nt.functionName === imp.functionName);
36234
+ const found = cached2.nodeTypes.find((nt) => nt.functionName === imp.functionName);
36219
36235
  if (found) {
36220
36236
  return { ...found, name: imp.name, importSource: imp.importSource };
36221
36237
  }
@@ -36245,7 +36261,8 @@ var init_parser2 = __esm({
36245
36261
  allNodeTypes.push(nodeType);
36246
36262
  }
36247
36263
  this.project.removeSourceFile(dtsFile);
36248
- this.importCache.set(cacheKey, allNodeTypes);
36264
+ const dtsMtime2 = fs5.statSync(dtsPath).mtimeMs;
36265
+ this.importCache.set(cacheKey, { mtime: dtsMtime2, nodeTypes: allNodeTypes });
36249
36266
  const found = allNodeTypes.find((nt) => nt.functionName === imp.functionName);
36250
36267
  if (found) {
36251
36268
  return { ...found, name: imp.name, importSource: imp.importSource };
@@ -95943,7 +95960,7 @@ function parseIntStrict(value) {
95943
95960
  // src/cli/index.ts
95944
95961
  init_logger();
95945
95962
  init_error_utils();
95946
- var version2 = true ? "0.24.1" : "0.0.0-dev";
95963
+ var version2 = true ? "0.24.3" : "0.0.0-dev";
95947
95964
  var program2 = new Command();
95948
95965
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
95949
95966
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.24.1";
1
+ export declare const VERSION = "0.24.3";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.24.1';
2
+ export const VERSION = '0.24.3';
3
3
  //# sourceMappingURL=generated-version.js.map
package/dist/parser.js CHANGED
@@ -399,10 +399,12 @@ export class AnnotationParser {
399
399
  throw new Error(`Circular dependency detected:\n ${cycle.join('\n -> ')}`);
400
400
  }
401
401
  try {
402
- // Check cache first
402
+ // Check cache first — validate mtime to detect file changes
403
403
  let nodeTypes;
404
- if (this.importCache.has(importedFilePath)) {
405
- nodeTypes = this.importCache.get(importedFilePath);
404
+ const importStats = fs.statSync(importedFilePath);
405
+ const cached = this.importCache.get(importedFilePath);
406
+ if (cached && cached.mtime === importStats.mtimeMs) {
407
+ nodeTypes = cached.nodeTypes;
406
408
  }
407
409
  else {
408
410
  // Add to import stack for circular dependency detection
@@ -428,8 +430,8 @@ export class AnnotationParser {
428
430
  nodeTypes.push(...inferredFromImport);
429
431
  // Clean up imported source file to prevent Project bloat
430
432
  this.project.removeSourceFile(importedFile);
431
- // Cache the parsed node types
432
- this.importCache.set(importedFilePath, nodeTypes);
433
+ // Cache the parsed node types with mtime for invalidation
434
+ this.importCache.set(importedFilePath, { mtime: importStats.mtimeMs, nodeTypes });
433
435
  }
434
436
  finally {
435
437
  // Remove from stack after processing
@@ -477,10 +479,25 @@ export class AnnotationParser {
477
479
  return [];
478
480
  const importedNames = new Set();
479
481
  namedImports.forEach((ni) => importedNames.add(ni.getName()));
480
- // Check cache
482
+ // Check cache (npm imports use package path mtime for invalidation)
481
483
  const cacheKey = `npm:${moduleSpecifier}`;
482
- if (this.importCache.has(cacheKey)) {
483
- return this.importCache.get(cacheKey).filter((nt) => importedNames.has(nt.functionName));
484
+ const npmCached = this.importCache.get(cacheKey);
485
+ if (npmCached) {
486
+ // For npm packages, check mtime of the resolved .d.ts file
487
+ const currentDir = path.dirname(currentFilePath);
488
+ const resolvedDts = resolvePackageTypesPath(moduleSpecifier, currentDir);
489
+ if (resolvedDts) {
490
+ try {
491
+ const dtsStats = fs.statSync(resolvedDts);
492
+ if (npmCached.mtime === dtsStats.mtimeMs) {
493
+ return npmCached.nodeTypes.filter((nt) => importedNames.has(nt.functionName));
494
+ }
495
+ }
496
+ catch { /* file gone — re-parse */ }
497
+ }
498
+ else {
499
+ return npmCached.nodeTypes.filter((nt) => importedNames.has(nt.functionName));
500
+ }
484
501
  }
485
502
  // Resolve .d.ts path
486
503
  const currentDir = path.dirname(currentFilePath);
@@ -509,8 +526,9 @@ export class AnnotationParser {
509
526
  }
510
527
  // Clean up the temporary source file
511
528
  this.project.removeSourceFile(dtsFile);
512
- // Cache all node types from this package
513
- this.importCache.set(cacheKey, allNodeTypes);
529
+ // Cache all node types from this package (with mtime of the .d.ts file)
530
+ const dtsMtime = fs.statSync(dtsPath).mtimeMs;
531
+ this.importCache.set(cacheKey, { mtime: dtsMtime, nodeTypes: allNodeTypes });
514
532
  // Return only the ones in the import statement
515
533
  return allNodeTypes.filter((nt) => importedNames.has(nt.functionName));
516
534
  }
@@ -596,7 +614,7 @@ export class AnnotationParser {
596
614
  const cacheKey = `npm:${imp.importSource}`;
597
615
  if (this.importCache.has(cacheKey)) {
598
616
  const cached = this.importCache.get(cacheKey);
599
- const found = cached.find((nt) => nt.functionName === imp.functionName);
617
+ const found = cached.nodeTypes.find((nt) => nt.functionName === imp.functionName);
600
618
  if (found) {
601
619
  // Return a copy with the correct name from @fwImport
602
620
  return { ...found, name: imp.name, importSource: imp.importSource };
@@ -627,8 +645,9 @@ export class AnnotationParser {
627
645
  allNodeTypes.push(nodeType);
628
646
  }
629
647
  this.project.removeSourceFile(dtsFile);
630
- // Cache all node types from this package
631
- this.importCache.set(cacheKey, allNodeTypes);
648
+ // Cache all node types from this package (with mtime of the .d.ts file)
649
+ const dtsMtime2 = fs.statSync(dtsPath).mtimeMs;
650
+ this.importCache.set(cacheKey, { mtime: dtsMtime2, nodeTypes: allNodeTypes });
632
651
  // Find the specific function we need
633
652
  const found = allNodeTypes.find((nt) => nt.functionName === imp.functionName);
634
653
  if (found) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",