@xynehq/jaf 0.1.19 → 0.1.21

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
@@ -89,6 +89,7 @@ examples/
89
89
  │ ├── index.ts # Demo entry point
90
90
  │ ├── rag-agent.ts # RAG agent implementation
91
91
  │ └── rag-tool.ts # RAG tool implementation
92
+ ├── hooks/turn-end-review.ts # Demonstrates awaiting onTurnEnd reviews between turns
92
93
  └── server-demo/ # Development server demo
93
94
  └── index.ts # Server demo entry point
94
95
  docs/ # Documentation
@@ -154,6 +155,10 @@ const config = {
154
155
  modelProvider,
155
156
  maxTurns: 10,
156
157
  onEvent: (event) => console.log(event), // Real-time tracing
158
+ onTurnEnd: async ({ turn, lastAssistantMessage }) => {
159
+ console.log(`Turn ${turn} completed:`, lastAssistantMessage?.content);
160
+ // Run reviews, persist breadcrumbs, throttle next turn, etc.
161
+ },
157
162
  };
158
163
 
159
164
  const initialState = {
@@ -168,6 +173,7 @@ const initialState = {
168
173
  const result = await run(initialState, config);
169
174
  ```
170
175
 
176
+
171
177
  ## 🔄 Function Composition
172
178
 
173
179
  JAF emphasizes function composition to build complex behaviors from simple, reusable functions:
@@ -619,4 +625,4 @@ npm run typecheck # Type checking
619
625
 
620
626
  ---
621
627
 
622
- **JAF** - Building the future of functional AI agent systems 🚀
628
+ **JAF** - Building the future of functional AI agent systems 🚀
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AACA,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EAET,UAAU,EAMX,MAAM,YAAY,CAAC;AAMpB,wBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,EAChC,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,EAC3B,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,GACrB,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAgEzB;AA8CD;;;;;;;GAOG;AACH,wBAAuB,SAAS,CAAC,GAAG,EAAE,GAAG,EACvC,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,EAC3B,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,EACtB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,GAC3E,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAwC3C"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AACA,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EAET,UAAU,EASX,MAAM,YAAY,CAAC;AA8CpB,wBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,EAChC,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,EAC3B,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,GACrB,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CA0EzB;AAsED;;;;;;;GAOG;AACH,wBAAuB,SAAS,CAAC,GAAG,EAAE,GAAG,EACvC,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,EAC3B,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,EACtB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,GAC3E,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAwC3C"}
@@ -1,7 +1,36 @@
1
- import { getTextContent, } from './types.js';
1
+ import { z } from 'zod';
2
+ import { getTextContent, InterruptionStatus, } from './types.js';
2
3
  import { setToolRuntime } from './tool-runtime.js';
3
4
  import { buildEffectiveGuardrails, executeInputGuardrailsParallel, executeInputGuardrailsSequential, executeOutputGuardrails } from './guardrails.js';
4
5
  import { safeConsole } from '../utils/logger.js';
6
+ import { DEFAULT_CLARIFICATION_DESCRIPTION } from '../utils/constants.js';
7
+ /**
8
+ * Create the built-in clarification tool
9
+ */
10
+ function createClarificationTool(config) {
11
+ const description = config.clarificationDescription || DEFAULT_CLARIFICATION_DESCRIPTION;
12
+ return {
13
+ schema: {
14
+ name: 'request_user_clarification',
15
+ description,
16
+ parameters: z.object({
17
+ question: z.string().describe('The clarifying question to ask the user'),
18
+ options: z.array(z.object({
19
+ id: z.string().describe('Unique identifier for this option'),
20
+ label: z.string().describe('Human-readable label shown to the user')
21
+ })).min(2).describe('clear and meaningful options that user can choose from (minimum 2 options)')
22
+ })
23
+ },
24
+ execute: async (args, _context) => {
25
+ const trigger = {
26
+ _clarification_trigger: true,
27
+ question: args.question,
28
+ options: args.options
29
+ };
30
+ return JSON.stringify(trigger);
31
+ }
32
+ };
33
+ }
5
34
  export async function run(initialState, config) {
6
35
  try {
7
36
  config.onEvent?.({
@@ -41,7 +70,12 @@ export async function run(initialState, config) {
41
70
  }
42
71
  config.onEvent?.({
43
72
  type: 'run_end',
44
- data: { outcome: result.outcome, traceId: initialState.traceId, runId: initialState.runId }
73
+ data: {
74
+ outcome: result.outcome,
75
+ finalState: result.finalState,
76
+ traceId: initialState.traceId,
77
+ runId: initialState.runId
78
+ }
45
79
  });
46
80
  return result;
47
81
  }
@@ -58,7 +92,12 @@ export async function run(initialState, config) {
58
92
  };
59
93
  config.onEvent?.({
60
94
  type: 'run_end',
61
- data: { outcome: errorResult.outcome, traceId: initialState.traceId, runId: initialState.runId }
95
+ data: {
96
+ outcome: errorResult.outcome,
97
+ finalState: errorResult.finalState,
98
+ traceId: initialState.traceId,
99
+ runId: initialState.runId
100
+ }
62
101
  });
63
102
  return errorResult;
64
103
  }
@@ -108,6 +147,20 @@ function createAsyncEventStream() {
108
147
  },
109
148
  };
110
149
  }
150
+ async function runTurnEndHooks(config, payload) {
151
+ config.onEvent?.({
152
+ type: 'turn_end',
153
+ data: { turn: payload.turn, agentName: payload.agentName }
154
+ });
155
+ if (config.onTurnEnd) {
156
+ await config.onTurnEnd({
157
+ turn: payload.turn,
158
+ agentName: payload.agentName,
159
+ state: payload.state,
160
+ lastAssistantMessage: payload.lastAssistantMessage
161
+ });
162
+ }
163
+ }
111
164
  /**
112
165
  * Stream run events as they happen via an async generator.
113
166
  * Consumers can iterate events to build live UIs or forward via SSE.
@@ -238,6 +291,48 @@ async function runInternal(state, config) {
238
291
  const resumed = await tryResumePendingToolCalls(state, config);
239
292
  if (resumed)
240
293
  return resumed;
294
+ // Check if we're resuming from a clarification
295
+ if (state.clarifications && state.clarifications.size > 0) {
296
+ const lastMessage = state.messages[state.messages.length - 1];
297
+ if (lastMessage?.role === 'tool') {
298
+ try {
299
+ const content = JSON.parse(getTextContent(lastMessage.content));
300
+ if (content.status === InterruptionStatus.AwaitingClarification) {
301
+ const clarificationId = content.clarification_id;
302
+ const selectedId = state.clarifications.get(clarificationId);
303
+ if (selectedId) {
304
+ safeConsole.log(`[JAF:ENGINE] Resuming with clarification: ${clarificationId}, selected option: ${selectedId}`);
305
+ // Find the selected option to include in the event
306
+ const updatedMessages = [...state.messages];
307
+ updatedMessages[updatedMessages.length - 1] = {
308
+ ...lastMessage,
309
+ content: JSON.stringify({
310
+ status: InterruptionStatus.ClarificationProvided,
311
+ message: `User selected option: ${selectedId}`
312
+ })
313
+ };
314
+ config.onEvent?.({
315
+ type: 'clarification_provided',
316
+ data: {
317
+ clarificationId,
318
+ selectedId,
319
+ selectedOption: { id: selectedId, label: selectedId }
320
+ }
321
+ });
322
+ // Continue execution with updated messages
323
+ const stateWithClarification = {
324
+ ...state,
325
+ messages: updatedMessages
326
+ };
327
+ return runInternal(stateWithClarification, config);
328
+ }
329
+ }
330
+ }
331
+ catch (e) {
332
+ safeConsole.log(`[JAF:ENGINE] Error checking for clarification resume:`, e);
333
+ }
334
+ }
335
+ }
241
336
  const maxTurns = config.maxTurns ?? 50;
242
337
  if (state.turnCount >= maxTurns) {
243
338
  return {
@@ -295,26 +390,35 @@ async function runInternal(state, config) {
295
390
  inputGuardrailsToRunLength: inputGuardrailsToRun.length,
296
391
  hasAdvancedGuardrails
297
392
  });
298
- safeConsole.log(`[JAF:ENGINE] Using agent: ${currentAgent.name}`);
299
- safeConsole.log(`[JAF:ENGINE] Agent has ${currentAgent.tools?.length || 0} tools available`);
300
- if (currentAgent.tools) {
301
- safeConsole.log(`[JAF:ENGINE] Available tools:`, currentAgent.tools.map(t => t.schema.name));
393
+ const effectiveTools = [
394
+ ...(currentAgent.tools || [])
395
+ ];
396
+ if (config.allowClarificationRequests) {
397
+ effectiveTools.push(createClarificationTool(config));
398
+ }
399
+ const effectiveAgent = {
400
+ ...currentAgent,
401
+ tools: effectiveTools
402
+ };
403
+ safeConsole.log(`[JAF:ENGINE] Using agent: ${effectiveAgent.name}`);
404
+ if (effectiveTools) {
405
+ safeConsole.log(`[JAF:ENGINE] Available tools:`, effectiveTools.map(t => t.schema.name));
302
406
  }
303
407
  config.onEvent?.({
304
408
  type: 'agent_processing',
305
409
  data: {
306
- agentName: currentAgent.name,
410
+ agentName: effectiveAgent.name,
307
411
  traceId: state.traceId,
308
412
  runId: state.runId,
309
413
  turnCount: state.turnCount,
310
414
  messageCount: state.messages.length,
311
- toolsAvailable: currentAgent.tools?.map(t => ({
415
+ toolsAvailable: effectiveTools.map(t => ({
312
416
  name: t.schema.name,
313
417
  description: t.schema.description
314
- })) || [],
315
- handoffsAvailable: currentAgent.handoffs || [],
316
- modelConfig: currentAgent.modelConfig,
317
- hasOutputCodec: !!currentAgent.outputCodec,
418
+ })),
419
+ handoffsAvailable: effectiveAgent.handoffs || [],
420
+ modelConfig: effectiveAgent.modelConfig,
421
+ hasOutputCodec: !!effectiveAgent.outputCodec,
318
422
  context: state.context,
319
423
  currentState: {
320
424
  messages: state.messages.map(m => ({
@@ -341,18 +445,18 @@ async function runInternal(state, config) {
341
445
  const turnNumber = state.turnCount + 1;
342
446
  config.onEvent?.({ type: 'turn_start', data: { turn: turnNumber, agentName: currentAgent.name } });
343
447
  const llmCallData = {
344
- agentName: currentAgent.name,
448
+ agentName: effectiveAgent.name,
345
449
  model: model || 'unknown',
346
450
  traceId: state.traceId,
347
451
  runId: state.runId,
348
452
  messages: state.messages,
349
- tools: currentAgent.tools?.map(tool => ({
453
+ tools: effectiveTools.map(tool => ({
350
454
  name: tool.schema.name,
351
455
  description: tool.schema.description,
352
456
  parameters: tool.schema.parameters
353
457
  })),
354
458
  modelConfig: {
355
- ...currentAgent.modelConfig,
459
+ ...effectiveAgent.modelConfig,
356
460
  modelOverride: config.modelOverride
357
461
  },
358
462
  turnCount: state.turnCount,
@@ -373,6 +477,12 @@ async function runInternal(state, config) {
373
477
  if (executionMode === 'sequential') {
374
478
  const guardrailResult = await executeInputGuardrailsSequential(inputGuardrailsToRun, firstUserMessage, config);
375
479
  if (!guardrailResult.isValid) {
480
+ await runTurnEndHooks(config, {
481
+ turn: turnNumber,
482
+ agentName: currentAgent.name,
483
+ state,
484
+ lastAssistantMessage: undefined
485
+ });
376
486
  return {
377
487
  finalState: state,
378
488
  outcome: {
@@ -385,11 +495,11 @@ async function runInternal(state, config) {
385
495
  };
386
496
  }
387
497
  safeConsole.log(`✅ All input guardrails passed. Starting LLM call.`);
388
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
498
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
389
499
  }
390
500
  else {
391
501
  const guardrailPromise = executeInputGuardrailsParallel(inputGuardrailsToRun, firstUserMessage, config);
392
- const llmPromise = config.modelProvider.getCompletion(state, currentAgent, config);
502
+ const llmPromise = config.modelProvider.getCompletion(state, effectiveAgent, config);
393
503
  const [guardrailResult, llmResult] = await Promise.all([
394
504
  guardrailPromise,
395
505
  llmPromise
@@ -398,6 +508,12 @@ async function runInternal(state, config) {
398
508
  if (!guardrailResult.isValid) {
399
509
  safeConsole.log(`🚨 Input guardrail violation: ${guardrailResult.errorMessage}`);
400
510
  safeConsole.log(`[JAF:GUARDRAILS] Discarding LLM response due to input guardrail violation`);
511
+ await runTurnEndHooks(config, {
512
+ turn: turnNumber,
513
+ agentName: currentAgent.name,
514
+ state,
515
+ lastAssistantMessage: undefined
516
+ });
401
517
  return {
402
518
  finalState: state,
403
519
  outcome: {
@@ -422,6 +538,12 @@ async function runInternal(state, config) {
422
538
  type: 'guardrail_violation',
423
539
  data: { stage: 'input', reason: errorMessage }
424
540
  });
541
+ await runTurnEndHooks(config, {
542
+ turn: turnNumber,
543
+ agentName: currentAgent.name,
544
+ state,
545
+ lastAssistantMessage: undefined
546
+ });
425
547
  return {
426
548
  finalState: state,
427
549
  outcome: {
@@ -434,14 +556,14 @@ async function runInternal(state, config) {
434
556
  };
435
557
  }
436
558
  }
437
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
559
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
438
560
  }
439
561
  }
440
562
  else {
441
563
  if (typeof config.modelProvider.getCompletionStream === 'function') {
442
564
  try {
443
565
  streamingUsed = true;
444
- const stream = config.modelProvider.getCompletionStream(state, currentAgent, config);
566
+ const stream = config.modelProvider.getCompletionStream(state, effectiveAgent, config);
445
567
  let aggregatedText = '';
446
568
  const toolCalls = [];
447
569
  for await (const chunk of stream) {
@@ -509,11 +631,11 @@ async function runInternal(state, config) {
509
631
  catch (e) {
510
632
  streamingUsed = false;
511
633
  assistantEventStreamed = false;
512
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
634
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
513
635
  }
514
636
  }
515
637
  else {
516
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
638
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
517
639
  }
518
640
  }
519
641
  }
@@ -521,7 +643,7 @@ async function runInternal(state, config) {
521
643
  if (typeof config.modelProvider.getCompletionStream === 'function') {
522
644
  try {
523
645
  streamingUsed = true;
524
- const stream = config.modelProvider.getCompletionStream(state, currentAgent, config);
646
+ const stream = config.modelProvider.getCompletionStream(state, effectiveAgent, config);
525
647
  let aggregatedText = '';
526
648
  const toolCalls = [];
527
649
  for await (const chunk of stream) {
@@ -589,11 +711,11 @@ async function runInternal(state, config) {
589
711
  catch (e) {
590
712
  streamingUsed = false;
591
713
  assistantEventStreamed = false;
592
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
714
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
593
715
  }
594
716
  }
595
717
  else {
596
- llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
718
+ llmResponse = await config.modelProvider.getCompletion(state, effectiveAgent, config);
597
719
  }
598
720
  }
599
721
  const usage = llmResponse?.usage;
@@ -631,7 +753,12 @@ async function runInternal(state, config) {
631
753
  }
632
754
  catch { /* ignore */ }
633
755
  if (!llmResponse.message) {
634
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
756
+ await runTurnEndHooks(config, {
757
+ turn: turnNumber,
758
+ agentName: currentAgent.name,
759
+ state,
760
+ lastAssistantMessage: undefined
761
+ });
635
762
  return {
636
763
  finalState: state,
637
764
  outcome: {
@@ -668,7 +795,7 @@ async function runInternal(state, config) {
668
795
  config.onEvent?.({ type: 'tool_requests', data: { toolCalls: requests } });
669
796
  }
670
797
  catch { /* ignore */ }
671
- const toolResults = await executeToolCalls(llmResponse.message.tool_calls, currentAgent, state, config);
798
+ const toolResults = await executeToolCalls(llmResponse.message.tool_calls, effectiveAgent, state, config);
672
799
  const interruptions = toolResults
673
800
  .map(r => r.interruption)
674
801
  .filter((interruption) => interruption !== undefined);
@@ -676,6 +803,7 @@ async function runInternal(state, config) {
676
803
  const completedToolResults = toolResults.filter(r => !r.interruption);
677
804
  const approvalRequiredResults = toolResults.filter(r => r.interruption);
678
805
  const updatedApprovals = new Map(state.approvals ?? []);
806
+ const updatedClarifications = new Map(state.clarifications ?? []);
679
807
  for (const interruption of interruptions) {
680
808
  if (interruption.type === 'tool_approval') {
681
809
  updatedApprovals.set(interruption.toolCall.id, {
@@ -684,12 +812,26 @@ async function runInternal(state, config) {
684
812
  additionalContext: { status: 'pending', timestamp: new Date().toISOString() }
685
813
  });
686
814
  }
815
+ else if (interruption.type === 'clarification_required') {
816
+ // Emit clarification requested event
817
+ config.onEvent?.({
818
+ type: 'clarification_requested',
819
+ data: {
820
+ clarificationId: interruption.clarificationId,
821
+ question: interruption.question,
822
+ options: interruption.options,
823
+ context: interruption.context
824
+ }
825
+ });
826
+ safeConsole.log(`[JAF:ENGINE] Clarification requested: ${interruption.question}`);
827
+ }
687
828
  }
688
829
  const interruptedState = {
689
830
  ...state,
690
831
  messages: [...newMessages, ...completedToolResults.map(r => r.message)],
691
832
  turnCount: updatedTurnCount,
692
833
  approvals: updatedApprovals,
834
+ clarifications: updatedClarifications,
693
835
  };
694
836
  if (config.memory?.autoStore && config.conversationId) {
695
837
  safeConsole.log(`[JAF:ENGINE] Storing conversation state due to interruption for ${config.conversationId}`);
@@ -699,6 +841,12 @@ async function runInternal(state, config) {
699
841
  };
700
842
  await storeConversationHistory(stateForStorage, config);
701
843
  }
844
+ await runTurnEndHooks(config, {
845
+ turn: turnNumber,
846
+ agentName: currentAgent.name,
847
+ state: interruptedState,
848
+ lastAssistantMessage: assistantMessage
849
+ });
702
850
  return {
703
851
  finalState: interruptedState,
704
852
  outcome: {
@@ -707,7 +855,7 @@ async function runInternal(state, config) {
707
855
  },
708
856
  };
709
857
  }
710
- safeConsole.log(`[JAF:ENGINE] Tool execution completed. Results count:`, toolResults.length);
858
+ // safeConsole.log(`[JAF:ENGINE] Tool execution completed. Results count:`, toolResults.length);
711
859
  config.onEvent?.({
712
860
  type: 'tool_results_to_llm',
713
861
  data: { results: toolResults.map(r => r.message) }
@@ -721,8 +869,15 @@ async function runInternal(state, config) {
721
869
  type: 'handoff_denied',
722
870
  data: { from: currentAgent.name, to: targetAgent, reason: `Agent ${currentAgent.name} cannot handoff to ${targetAgent}` }
723
871
  });
872
+ const failureState = { ...state, messages: newMessages, turnCount: updatedTurnCount };
873
+ await runTurnEndHooks(config, {
874
+ turn: turnNumber,
875
+ agentName: currentAgent.name,
876
+ state: failureState,
877
+ lastAssistantMessage: assistantMessage
878
+ });
724
879
  return {
725
- finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
880
+ finalState: failureState,
726
881
  outcome: {
727
882
  status: 'error',
728
883
  error: {
@@ -759,7 +914,12 @@ async function runInternal(state, config) {
759
914
  turnCount: updatedTurnCount,
760
915
  approvals: state.approvals ?? new Map(),
761
916
  };
762
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
917
+ await runTurnEndHooks(config, {
918
+ turn: turnNumber,
919
+ agentName: currentAgent.name,
920
+ state: nextState,
921
+ lastAssistantMessage: assistantMessage
922
+ });
763
923
  return runInternal(nextState, config);
764
924
  }
765
925
  }
@@ -785,7 +945,12 @@ async function runInternal(state, config) {
785
945
  turnCount: updatedTurnCount,
786
946
  approvals: state.approvals ?? new Map(),
787
947
  };
788
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
948
+ await runTurnEndHooks(config, {
949
+ turn: turnNumber,
950
+ agentName: currentAgent.name,
951
+ state: nextState,
952
+ lastAssistantMessage: assistantMessage
953
+ });
789
954
  return runInternal(nextState, config);
790
955
  }
791
956
  if (llmResponse.message.content) {
@@ -793,7 +958,12 @@ async function runInternal(state, config) {
793
958
  const parseResult = currentAgent.outputCodec.safeParse(tryParseJSON(llmResponse.message.content));
794
959
  if (!parseResult.success) {
795
960
  config.onEvent?.({ type: 'decode_error', data: { errors: parseResult.error.issues } });
796
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
961
+ await runTurnEndHooks(config, {
962
+ turn: turnNumber,
963
+ agentName: currentAgent.name,
964
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
965
+ lastAssistantMessage: assistantMessage
966
+ });
797
967
  return {
798
968
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
799
969
  outcome: {
@@ -825,7 +995,12 @@ async function runInternal(state, config) {
825
995
  }
826
996
  }
827
997
  if (!outputGuardrailResult.isValid) {
828
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
998
+ await runTurnEndHooks(config, {
999
+ turn: turnNumber,
1000
+ agentName: currentAgent.name,
1001
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
1002
+ lastAssistantMessage: assistantMessage
1003
+ });
829
1004
  return {
830
1005
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
831
1006
  outcome: {
@@ -839,7 +1014,12 @@ async function runInternal(state, config) {
839
1014
  }
840
1015
  config.onEvent?.({ type: 'final_output', data: { output: parseResult.data } });
841
1016
  // End of turn
842
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
1017
+ await runTurnEndHooks(config, {
1018
+ turn: turnNumber,
1019
+ agentName: currentAgent.name,
1020
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
1021
+ lastAssistantMessage: assistantMessage
1022
+ });
843
1023
  return {
844
1024
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
845
1025
  outcome: {
@@ -869,7 +1049,12 @@ async function runInternal(state, config) {
869
1049
  }
870
1050
  }
871
1051
  if (!outputGuardrailResult.isValid) {
872
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
1052
+ await runTurnEndHooks(config, {
1053
+ turn: turnNumber,
1054
+ agentName: currentAgent.name,
1055
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
1056
+ lastAssistantMessage: assistantMessage
1057
+ });
873
1058
  return {
874
1059
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
875
1060
  outcome: {
@@ -883,7 +1068,12 @@ async function runInternal(state, config) {
883
1068
  }
884
1069
  config.onEvent?.({ type: 'final_output', data: { output: llmResponse.message.content } });
885
1070
  // End of turn
886
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
1071
+ await runTurnEndHooks(config, {
1072
+ turn: turnNumber,
1073
+ agentName: currentAgent.name,
1074
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
1075
+ lastAssistantMessage: assistantMessage
1076
+ });
887
1077
  return {
888
1078
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
889
1079
  outcome: {
@@ -893,7 +1083,12 @@ async function runInternal(state, config) {
893
1083
  };
894
1084
  }
895
1085
  }
896
- config.onEvent?.({ type: 'turn_end', data: { turn: turnNumber, agentName: currentAgent.name } });
1086
+ await runTurnEndHooks(config, {
1087
+ turn: turnNumber,
1088
+ agentName: currentAgent.name,
1089
+ state: { ...state, messages: newMessages, turnCount: updatedTurnCount },
1090
+ lastAssistantMessage: assistantMessage
1091
+ });
897
1092
  safeConsole.error(`[JAF:ENGINE] No tool calls or content returned by model. LLMResponse: `, llmResponse);
898
1093
  return {
899
1094
  finalState: { ...state, messages: newMessages, turnCount: updatedTurnCount },
@@ -938,14 +1133,10 @@ async function executeToolCalls(toolCalls, agent, state, config) {
938
1133
  });
939
1134
  // If event handler returns a value, use it to override the args
940
1135
  if (beforeEventResponse !== undefined && beforeEventResponse !== null) {
941
- console.log(`[JAF:ENGINE] Tool args modified by before_tool_execution event handler for ${toolCall.function.name}`);
942
- console.log(`[JAF:ENGINE] Original args:`, rawArgs);
943
- console.log(`[JAF:ENGINE] Modified args:`, beforeEventResponse);
944
1136
  rawArgs = beforeEventResponse;
945
1137
  }
946
1138
  }
947
1139
  catch (eventError) {
948
- console.error(`[JAF:ENGINE] Error in before_tool_execution event handler:`, eventError);
949
1140
  // Continue with original args if event handler fails
950
1141
  }
951
1142
  }
@@ -1079,6 +1270,38 @@ async function executeToolCalls(toolCalls, agent, state, config) {
1079
1270
  ? { ...state.context, ...additionalContext }
1080
1271
  : state.context;
1081
1272
  let toolResult = await tool.execute(parseResult.data, contextWithAdditional);
1273
+ // Check if this is a clarification request
1274
+ // The clarification tool returns a JSON string containing the trigger marker
1275
+ if (typeof toolResult === 'string') {
1276
+ try {
1277
+ const parsed = JSON.parse(toolResult);
1278
+ if (parsed && typeof parsed === 'object' && '_clarification_trigger' in parsed && parsed._clarification_trigger === true) {
1279
+ const clarificationId = `clarify_${toolCall.id}`;
1280
+ const trigger = parsed;
1281
+ return {
1282
+ interruption: {
1283
+ type: 'clarification_required',
1284
+ clarificationId,
1285
+ question: trigger.question,
1286
+ options: trigger.options,
1287
+ context: trigger.context
1288
+ },
1289
+ message: {
1290
+ role: 'tool',
1291
+ content: JSON.stringify({
1292
+ status: InterruptionStatus.AwaitingClarification,
1293
+ clarification_id: clarificationId,
1294
+ message: 'Waiting for user to provide clarification'
1295
+ }),
1296
+ tool_call_id: toolCall.id
1297
+ }
1298
+ };
1299
+ }
1300
+ }
1301
+ catch {
1302
+ // Not a clarification trigger, continue with normal processing
1303
+ }
1304
+ }
1082
1305
  // Apply onAfterToolExecution callback if configured
1083
1306
  if (config.onAfterToolExecution) {
1084
1307
  try {
@@ -1104,14 +1327,13 @@ async function executeToolCalls(toolCalls, agent, state, config) {
1104
1327
  let toolResultObj = null;
1105
1328
  if (typeof toolResult === 'string') {
1106
1329
  resultString = toolResult;
1107
- safeConsole.log(`[JAF:ENGINE] Tool ${toolCall.function.name} returned string:`, resultString);
1330
+ safeConsole.log(`[JAF:ENGINE] Tool ${toolCall.function.name}`);
1108
1331
  }
1109
1332
  else {
1110
1333
  toolResultObj = toolResult;
1111
1334
  const { toolResultToString } = await import('./tool-results');
1112
1335
  resultString = toolResultToString(toolResult);
1113
- safeConsole.log(`[JAF:ENGINE] Tool ${toolCall.function.name} returned ToolResult:`, toolResult);
1114
- safeConsole.log(`[JAF:ENGINE] Converted to string:`, resultString);
1336
+ safeConsole.log(`[JAF:ENGINE] Tool ${toolCall.function.name} `);
1115
1337
  }
1116
1338
  config.onEvent?.({
1117
1339
  type: 'tool_call_end',