agent-state-machine 2.0.12 → 2.0.13

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/lib/llm.js CHANGED
@@ -51,6 +51,7 @@ export function buildPrompt(context, options) {
51
51
  delete cleanContext._steering;
52
52
  delete cleanContext._loop;
53
53
  delete cleanContext._config;
54
+ delete cleanContext._memory;
54
55
 
55
56
  // Add the actual prompt
56
57
  parts.push('# Task\n\n');
@@ -70,13 +71,22 @@ export function buildPrompt(context, options) {
70
71
  parts.push('{ "interact": "your question here" }\n\n');
71
72
  parts.push('Only use this format when you genuinely need user input to proceed.\n\n---\n');
72
73
 
73
- // Add global steering if available
74
+ // Add global steering if available (always first)
74
75
  if (context._steering?.global) {
75
76
  parts.push('# System Instructions\n');
76
77
  parts.push(context._steering.global);
77
78
  parts.push('\n---\n');
78
79
  }
79
80
 
81
+ // Add additional steering files if available
82
+ if (context._steering?.additional && context._steering.additional.length > 0) {
83
+ parts.push('# Additional Guidelines\n');
84
+ for (const content of context._steering.additional) {
85
+ parts.push(content);
86
+ parts.push('\n---\n');
87
+ }
88
+ }
89
+
80
90
  return parts.join('\n');
81
91
  }
82
92
 
@@ -17,50 +17,87 @@ const require = createRequire(import.meta.url);
17
17
  /**
18
18
  * Run an agent with context
19
19
  * @param {string} name - Agent name (file basename)
20
- * @param {object} params - Parameters passed to agent
20
+ * @param {object} params - Parameters passed to agent (default: {})
21
+ * @param {object} options - Agent execution options (default: {})
22
+ * @param {number|false} options.retry - Number of retries (default: 2, meaning 3 total attempts). Set to false to disable.
23
+ * @param {string|string[]} options.steering - Additional steering files to load from steering/ folder
21
24
  */
22
- export async function agent(name, params = {}) {
25
+ export async function agent(name, params = {}, options = {}) {
23
26
  const runtime = getCurrentRuntime();
24
27
  if (!runtime) {
25
28
  throw new Error('agent() must be called within a workflow context');
26
29
  }
27
30
 
28
- console.log(` [Agent: ${name}] Starting...`);
31
+ // Parse retry option: default is 2 retries (3 total attempts)
32
+ const retryCount = options.retry === false ? 0 : (options.retry ?? 2);
29
33
 
30
- try {
31
- const result = await executeAgent(runtime, name, params);
34
+ let lastError;
32
35
 
33
- if (result && typeof result === 'object' && result._debug_prompt) {
34
- delete result._debug_prompt;
35
- }
36
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
37
+ try {
38
+ if (attempt > 0) {
39
+ console.log(` [Agent: ${name}] Retry attempt ${attempt}/${retryCount}...`);
40
+ } else {
41
+ console.log(` [Agent: ${name}] Starting...`);
42
+ }
43
+
44
+ const result = await executeAgent(runtime, name, params, options);
45
+
46
+ if (result && typeof result === 'object' && result._debug_prompt) {
47
+ delete result._debug_prompt;
48
+ }
49
+
50
+ console.log(` [Agent: ${name}] Completed`);
51
+ if (runtime._agentSuppressCompletion?.has(name)) {
52
+ runtime._agentSuppressCompletion.delete(name);
53
+ return result;
54
+ }
55
+
56
+ runtime.prependHistory({
57
+ event: 'AGENT_COMPLETED',
58
+ agent: name,
59
+ output: result,
60
+ attempts: attempt + 1
61
+ });
36
62
 
37
- console.log(` [Agent: ${name}] Completed`);
38
- if (runtime._agentSuppressCompletion?.has(name)) {
39
- runtime._agentSuppressCompletion.delete(name);
40
63
  return result;
64
+ } catch (error) {
65
+ lastError = error;
66
+
67
+ if (attempt < retryCount) {
68
+ console.error(` [Agent: ${name}] Error (attempt ${attempt + 1}/${retryCount + 1}): ${error.message}`);
69
+ runtime.prependHistory({
70
+ event: 'AGENT_RETRY',
71
+ agent: name,
72
+ attempt: attempt + 1,
73
+ error: error.message
74
+ });
75
+ }
41
76
  }
77
+ }
42
78
 
43
- runtime.prependHistory({
44
- event: 'AGENT_COMPLETED',
45
- agent: name,
46
- output: result
47
- });
79
+ // All retries exhausted - record failure
80
+ runtime.prependHistory({
81
+ event: 'AGENT_FAILED',
82
+ agent: name,
83
+ error: lastError.message,
84
+ attempts: retryCount + 1
85
+ });
48
86
 
49
- return result;
50
- } catch (error) {
51
- runtime.prependHistory({
52
- event: 'AGENT_FAILED',
53
- agent: name,
54
- error: error.message
55
- });
56
- throw error;
57
- }
87
+ // Store error in accessible location (not auto-spread to context)
88
+ runtime._agentErrors.push({
89
+ agent: name,
90
+ error: lastError.message,
91
+ timestamp: new Date().toISOString()
92
+ });
93
+
94
+ throw lastError;
58
95
  }
59
96
 
60
97
  /**
61
98
  * Execute an agent (load and run)
62
99
  */
63
- export async function executeAgent(runtime, name, params) {
100
+ export async function executeAgent(runtime, name, params, options = {}) {
64
101
  const agentsDir = runtime.agentsDir;
65
102
 
66
103
  // Try JS agents (.js/.mjs/.cjs)
@@ -72,14 +109,14 @@ export async function executeAgent(runtime, name, params) {
72
109
 
73
110
  for (const p of jsCandidates) {
74
111
  if (fs.existsSync(p)) {
75
- return executeJSAgent(runtime, p, name, params);
112
+ return executeJSAgent(runtime, p, name, params, options);
76
113
  }
77
114
  }
78
115
 
79
116
  // Try Markdown agent
80
117
  const mdPath = path.join(agentsDir, `${name}.md`);
81
118
  if (fs.existsSync(mdPath)) {
82
- return executeMDAgent(runtime, mdPath, name, params);
119
+ return executeMDAgent(runtime, mdPath, name, params, options);
83
120
  }
84
121
 
85
122
  throw new Error(
@@ -94,7 +131,7 @@ export async function executeAgent(runtime, name, params) {
94
131
  * - ESM (.js/.mjs): loaded via dynamic import with cache-bust for hot reload
95
132
  * - CJS (.cjs): loaded via require() with cache clear
96
133
  */
97
- async function executeJSAgent(runtime, agentPath, name, params) {
134
+ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
98
135
  const ext = path.extname(agentPath).toLowerCase();
99
136
 
100
137
  let agentModule;
@@ -119,11 +156,15 @@ async function executeJSAgent(runtime, agentPath, name, params) {
119
156
 
120
157
  logAgentStart(runtime, name);
121
158
 
122
- // Build context
159
+ // Build steering context (global + any additional files from options)
160
+ const steeringContext = options.steering
161
+ ? runtime.loadSteeringFiles(options.steering)
162
+ : runtime.steering;
163
+
164
+ // Build context - only spread params, NOT memory (explicit context passing)
123
165
  const context = {
124
- ...runtime._rawMemory,
125
166
  ...params,
126
- _steering: runtime.steering,
167
+ _steering: steeringContext,
127
168
  _config: {
128
169
  models: runtime.workflowConfig.models,
129
170
  apiKeys: runtime.workflowConfig.apiKeys,
@@ -175,7 +216,7 @@ async function executeJSAgent(runtime, agentPath, name, params) {
175
216
  /**
176
217
  * Execute a Markdown agent (prompt-based)
177
218
  */
178
- async function executeMDAgent(runtime, agentPath, name, params) {
219
+ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
179
220
  const { llm, buildPrompt, parseJSON, parseInteractionRequest } = await import('../llm.js');
180
221
 
181
222
  const content = fs.readFileSync(agentPath, 'utf-8');
@@ -184,11 +225,28 @@ async function executeMDAgent(runtime, agentPath, name, params) {
184
225
  const outputKey = config.output || 'result';
185
226
  const targetKey = config.interactionKey || outputKey;
186
227
 
187
- // Build context
228
+ // Combine steering from options (runtime call) and frontmatter (static)
229
+ let steeringNames = [];
230
+
231
+ if (options.steering) {
232
+ const optSteering = Array.isArray(options.steering) ? options.steering : [options.steering];
233
+ steeringNames.push(...optSteering);
234
+ }
235
+
236
+ if (config.steering) {
237
+ const fmSteering = parseSteeringFrontmatter(config.steering);
238
+ steeringNames.push(...fmSteering);
239
+ }
240
+
241
+ // Build steering context (global + any additional files)
242
+ const steeringContext = steeringNames.length > 0
243
+ ? runtime.loadSteeringFiles(steeringNames)
244
+ : runtime.steering;
245
+
246
+ // Build context - only spread params, NOT memory (explicit context passing)
188
247
  const context = {
189
- ...runtime._rawMemory,
190
248
  ...params,
191
- _steering: runtime.steering,
249
+ _steering: steeringContext,
192
250
  _config: {
193
251
  models: runtime.workflowConfig.models,
194
252
  apiKeys: runtime.workflowConfig.apiKeys,
@@ -296,6 +354,36 @@ function parseMarkdownAgent(content) {
296
354
  return { config: {}, prompt: content.trim() };
297
355
  }
298
356
 
357
+ /**
358
+ * Parse steering value from frontmatter
359
+ * Supports: "name", "name1, name2", "[name1, name2]", "['name1', 'name2']"
360
+ * @param {string} value - Frontmatter steering value
361
+ * @returns {string[]} Array of steering file names
362
+ */
363
+ function parseSteeringFrontmatter(value) {
364
+ if (!value) return [];
365
+
366
+ const trimmed = value.trim();
367
+
368
+ // Handle array format: [a, b, c] or ["a", "b", "c"]
369
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
370
+ const inner = trimmed.slice(1, -1);
371
+ return inner.split(',')
372
+ .map(s => s.trim().replace(/^["']|["']$/g, ''))
373
+ .filter(Boolean);
374
+ }
375
+
376
+ // Handle comma-separated: a, b, c
377
+ if (trimmed.includes(',')) {
378
+ return trimmed.split(',')
379
+ .map(s => s.trim())
380
+ .filter(Boolean);
381
+ }
382
+
383
+ // Single value
384
+ return [trimmed];
385
+ }
386
+
299
387
  /**
300
388
  * Interpolate {{variables}} in prompt template
301
389
  */
@@ -89,6 +89,9 @@ export class WorkflowRuntime {
89
89
  // Agent interaction tracking for history logging
90
90
  this._agentResumeFlags = new Set();
91
91
  this._agentSuppressCompletion = new Set();
92
+
93
+ // Agent error tracking (not persisted to memory, but accessible during run)
94
+ this._agentErrors = [];
92
95
  }
93
96
 
94
97
  ensureDirectories() {
@@ -142,6 +145,39 @@ export class WorkflowRuntime {
142
145
  return steering;
143
146
  }
144
147
 
148
+ /**
149
+ * Load a specific steering file by name
150
+ * @param {string} name - Name of the steering file (without .md extension)
151
+ * @returns {string} Content of the steering file, or empty string if not found
152
+ */
153
+ loadSteeringFile(name) {
154
+ const filePath = path.join(this.steeringDir, `${name}.md`);
155
+
156
+ if (fs.existsSync(filePath)) {
157
+ return fs.readFileSync(filePath, 'utf-8');
158
+ }
159
+
160
+ console.warn(`${C.yellow}Warning: Steering file not found: ${name}.md${C.reset}`);
161
+ return '';
162
+ }
163
+
164
+ /**
165
+ * Load multiple steering files and combine with global
166
+ * @param {string|string[]} steeringNames - Names of steering files to load
167
+ * @returns {{ enabled: boolean, global: string, additional: string[] }}
168
+ */
169
+ loadSteeringFiles(steeringNames) {
170
+ const names = Array.isArray(steeringNames) ? steeringNames : [steeringNames];
171
+ const additional = names
172
+ .map(name => this.loadSteeringFile(name))
173
+ .filter(content => content.length > 0);
174
+
175
+ return {
176
+ ...this.steering,
177
+ additional
178
+ };
179
+ }
180
+
145
181
  /**
146
182
  * Persist state to disk
147
183
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",