@looopy-ai/core 1.1.0 → 1.1.2

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.
@@ -26,8 +26,6 @@ export declare class Agent {
26
26
  private readonly config;
27
27
  private _state;
28
28
  private logger;
29
- private sigtermHandler?;
30
- private sigintHandler?;
31
29
  private shuttingDown;
32
30
  private shutdownComplete;
33
31
  constructor(config: AgentConfig);
@@ -48,6 +46,4 @@ export declare class Agent {
48
46
  private loadPersistedState;
49
47
  private loadMessages;
50
48
  private checkAndCompact;
51
- private registerSignalHandlers;
52
- private removeSignalHandlers;
53
49
  }
@@ -2,15 +2,12 @@ import { catchError, concat, Observable, of, tap } from 'rxjs';
2
2
  import { createTaskStatusEvent } from '../events';
3
3
  import { addMessagesCompactedEvent, addMessagesLoadedEvent, completeAgentInitializeSpan, completeAgentTurnSpan, failAgentInitializeSpan, failAgentTurnSpan, setResumeAttributes, setTurnCountAttribute, startAgentInitializeSpan, startAgentTurnSpan, } from '../observability/spans';
4
4
  import { serializeError } from '../utils/error';
5
- import { registerSignalListener, unregisterSignalListener } from '../utils/process-signals';
6
5
  import { getLogger } from './logger';
7
6
  import { runLoop } from './loop';
8
7
  export class Agent {
9
8
  config;
10
9
  _state;
11
10
  logger;
12
- sigtermHandler;
13
- sigintHandler;
14
11
  shuttingDown = false;
15
12
  shutdownComplete = false;
16
13
  constructor(config) {
@@ -31,7 +28,6 @@ export class Agent {
31
28
  };
32
29
  this.logger.debug('Agent created');
33
30
  this.persistStateSafely();
34
- this.registerSignalHandlers();
35
31
  }
36
32
  get state() {
37
33
  return { ...this._state };
@@ -286,7 +282,6 @@ export class Agent {
286
282
  this._state.status = 'shutdown';
287
283
  this._state.lastActivity = new Date();
288
284
  await this.persistState();
289
- this.removeSignalHandlers();
290
285
  this.shutdownComplete = true;
291
286
  this.config.logger.info('Agent shutdown complete');
292
287
  }
@@ -371,46 +366,4 @@ export class Agent {
371
366
  });
372
367
  }
373
368
  }
374
- registerSignalHandlers() {
375
- if (typeof process === 'undefined') {
376
- return;
377
- }
378
- if (this.sigtermHandler || typeof process === 'undefined') {
379
- return;
380
- }
381
- if (!this.sigtermHandler) {
382
- this.sigtermHandler = async () => {
383
- this.logger.info('Received SIGTERM signal. Initiating graceful shutdown.');
384
- try {
385
- await this.shutdown();
386
- }
387
- catch (error) {
388
- this.logger.error({ error }, 'Failed to shutdown agent after SIGTERM signal');
389
- }
390
- };
391
- registerSignalListener('SIGTERM', this.sigtermHandler);
392
- }
393
- if (!this.sigintHandler) {
394
- const sigintHandler = async () => {
395
- this.logger.info('Received SIGINT signal. Initiating graceful shutdown.');
396
- try {
397
- await this.shutdown();
398
- }
399
- catch (error) {
400
- this.logger.error({ error }, 'Failed to shutdown agent after SIGINT signal');
401
- }
402
- };
403
- registerSignalListener('SIGINT', sigintHandler);
404
- }
405
- }
406
- removeSignalHandlers() {
407
- if (this.sigtermHandler) {
408
- unregisterSignalListener('SIGTERM', this.sigtermHandler);
409
- this.sigtermHandler = undefined;
410
- }
411
- if (this.sigintHandler) {
412
- unregisterSignalListener('SIGINT', this.sigintHandler);
413
- this.sigintHandler = undefined;
414
- }
415
- }
416
369
  }
@@ -1,5 +1,4 @@
1
- import { type Observable } from 'rxjs';
2
1
  import type { AnyEvent } from '../types/event';
3
2
  import type { Message } from '../types/message';
4
3
  import type { LoopConfig, TurnContext } from './types';
5
- export declare const runLoop: (context: TurnContext, config: LoopConfig, history: Message[]) => Observable<AnyEvent>;
4
+ export declare const runLoop: (context: TurnContext, config: LoopConfig, history: Message[]) => import("rxjs").Observable<AnyEvent>;
package/dist/core/loop.js CHANGED
@@ -1,6 +1,7 @@
1
- import { concat, EMPTY, expand, map, mergeMap, of, reduce, share, shareReplay, } from 'rxjs';
1
+ import { concat, EMPTY, mergeMap, of, reduce, shareReplay } from 'rxjs';
2
2
  import { createTaskCompleteEvent, createTaskCreatedEvent, createTaskStatusEvent } from '../events';
3
3
  import { startAgentLoopSpan } from '../observability/spans';
4
+ import { recursiveMerge } from '../utils/recursive-merge';
4
5
  import { runIteration } from './iteration';
5
6
  export const runLoop = (context, config, history) => {
6
7
  const logger = context.logger.child({ component: 'loop' });
@@ -49,34 +50,6 @@ export const runLoop = (context, config, history) => {
49
50
  }));
50
51
  return concat(of(taskEvent, workingEvent), merged$, finalSummary$).pipe(tapFinish);
51
52
  };
52
- function recursiveMerge(initial, eventsFor, next, isStop) {
53
- const seed = {
54
- state: initial,
55
- iteration: 0,
56
- events$: eventsFor({ ...initial, iteration: 0 }).pipe(shareReplay()),
57
- };
58
- const iterations$ = of(seed).pipe(expand(({ state, iteration, events$ }) => events$.pipe(reduce((acc, e) => {
59
- acc.events.push(e);
60
- if (isStop(e))
61
- acc.sawStop = true;
62
- return acc;
63
- }, { events: [], sawStop: false }), mergeMap(({ events, sawStop }) => {
64
- if (sawStop)
65
- return EMPTY;
66
- return of(next(state, { iteration, events })).pipe(map((nextState) => {
67
- const nextIter = iteration + 1;
68
- return {
69
- state: nextState,
70
- iteration: nextIter,
71
- events$: eventsFor({
72
- ...nextState,
73
- iteration: nextIter,
74
- }).pipe(share()),
75
- };
76
- }));
77
- }))));
78
- return iterations$.pipe(mergeMap(({ events$ }) => events$));
79
- }
80
53
  const eventsToMessages = (events) => {
81
54
  const messages = [];
82
55
  for (const event of events) {
@@ -88,22 +61,13 @@ const eventsToMessages = (events) => {
88
61
  content: event.content,
89
62
  });
90
63
  }
91
- break;
92
- case 'tool-call':
93
- messages.push({
94
- role: 'assistant',
95
- content: '',
96
- toolCalls: [
97
- {
98
- id: event.toolCallId,
99
- type: 'function',
100
- function: {
101
- name: event.toolName,
102
- arguments: event.arguments,
103
- },
104
- },
105
- ],
106
- });
64
+ if (event.finishReason === 'tool_calls' && (event.toolCalls?.length ?? 0) > 0) {
65
+ messages.push({
66
+ role: 'assistant',
67
+ content: '',
68
+ toolCalls: event.toolCalls,
69
+ });
70
+ }
107
71
  break;
108
72
  case 'tool-complete':
109
73
  messages.push({
@@ -43,7 +43,9 @@ export const runToolCall = (context, toolCall) => {
43
43
  logger.trace({
44
44
  success: result.success,
45
45
  }, 'Tool execution complete');
46
- return createToolCompleteEvent(context, toolCall, result.result);
46
+ return result.success
47
+ ? createToolCompleteEvent(context, toolCall, result.result)
48
+ : createToolErrorEvent(context, toolCall, result.error || 'Unknown error');
47
49
  }
48
50
  catch (error) {
49
51
  const err = error instanceof Error ? error : new Error(String(error));
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export * from './server';
6
6
  export * from './stores';
7
7
  export * from './tools';
8
8
  export * from './types';
9
+ export * from './utils';
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@ export * from './server';
6
6
  export * from './stores';
7
7
  export * from './tools';
8
8
  export * from './types';
9
+ export * from './utils';
@@ -1,3 +1,4 @@
1
1
  export { type BufferedEvent, EventBuffer, type EventBufferConfig, } from './event-buffer';
2
2
  export { type EventFilter, EventRouter, type Subscriber, type SubscriptionConfig, } from './event-router';
3
+ export * from './shutdown';
3
4
  export { SSEConnection, type SSEConnectionConfig, type SSEResponse, SSEServer, type SSEServerConfig, } from './sse';
@@ -1,3 +1,4 @@
1
1
  export { EventBuffer, } from './event-buffer';
2
2
  export { EventRouter, } from './event-router';
3
+ export * from './shutdown';
3
4
  export { SSEConnection, SSEServer, } from './sse';
@@ -0,0 +1,7 @@
1
+ export declare class ShutdownManager {
2
+ private watchers;
3
+ constructor();
4
+ registerWatcher(handleShutdown: () => Promise<void>, order?: number): void;
5
+ initiateShutdown(): Promise<void>;
6
+ private signalHandler;
7
+ }
@@ -0,0 +1,21 @@
1
+ import { getLogger } from '../core';
2
+ export class ShutdownManager {
3
+ watchers = [];
4
+ constructor() {
5
+ process.on('SIGINT', this.signalHandler.bind(this));
6
+ process.on('SIGTERM', this.signalHandler.bind(this));
7
+ }
8
+ registerWatcher(handleShutdown, order = 100) {
9
+ this.watchers.push({ handleShutdown, order });
10
+ this.watchers.sort((a, b) => a.order - b.order);
11
+ }
12
+ async initiateShutdown() {
13
+ for (const watcher of this.watchers) {
14
+ await watcher.handleShutdown();
15
+ }
16
+ }
17
+ signalHandler(signal) {
18
+ getLogger({ component: 'shutdown-manager' }).info({ signal, watchers: this.watchers.length }, 'Received shutdown signal');
19
+ this.initiateShutdown();
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export * from './error';
@@ -0,0 +1 @@
1
+ export * from './error';
@@ -0,0 +1,7 @@
1
+ import { type Observable } from 'rxjs';
2
+ export declare function recursiveMerge<S, E>(initial: S, eventsFor: (state: S & {
3
+ iteration: number;
4
+ }) => Observable<E>, next: (state: S, info: {
5
+ iteration: number;
6
+ events: E[];
7
+ }) => S, isStop: (e: E) => boolean): Observable<E>;
@@ -0,0 +1,29 @@
1
+ import { EMPTY, expand, map, mergeMap, of, reduce, share, shareReplay, } from 'rxjs';
2
+ export function recursiveMerge(initial, eventsFor, next, isStop) {
3
+ const seed = {
4
+ state: initial,
5
+ iteration: 0,
6
+ events$: eventsFor({ ...initial, iteration: 0 }).pipe(shareReplay()),
7
+ };
8
+ const iterations$ = of(seed).pipe(expand(({ state, iteration, events$ }) => events$.pipe(reduce((acc, e) => {
9
+ acc.events.push(e);
10
+ if (isStop(e))
11
+ acc.sawStop = true;
12
+ return acc;
13
+ }, { events: [], sawStop: false }), mergeMap(({ events, sawStop }) => {
14
+ if (sawStop)
15
+ return EMPTY;
16
+ return of(next(state, { iteration, events })).pipe(map((nextState) => {
17
+ const nextIter = iteration + 1;
18
+ return {
19
+ state: nextState,
20
+ iteration: nextIter,
21
+ events$: eventsFor({
22
+ ...nextState,
23
+ iteration: nextIter,
24
+ }).pipe(share()),
25
+ };
26
+ }));
27
+ }))));
28
+ return iterations$.pipe(mergeMap(({ events$ }) => events$));
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looopy-ai/core",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "RxJS-based AI agent framework",
5
5
  "keywords": [
6
6
  "agent",
@@ -1,3 +0,0 @@
1
- export type SignalListener = () => void | Promise<void>;
2
- export declare function registerSignalListener(signal: NodeJS.Signals, listener: SignalListener): void;
3
- export declare function unregisterSignalListener(signal: NodeJS.Signals, listener: SignalListener): void;
@@ -1,67 +0,0 @@
1
- import { getLogger } from '../core/logger';
2
- import { serializeError } from './error';
3
- const listeners = new Map();
4
- const processHandlers = new Map();
5
- const signalLogger = getLogger({ component: 'process-signal-coordinator' });
6
- function isProcessAvailable() {
7
- return typeof process !== 'undefined' && typeof process.on === 'function';
8
- }
9
- export function registerSignalListener(signal, listener) {
10
- if (!isProcessAvailable()) {
11
- return;
12
- }
13
- const signalListeners = listeners.get(signal) ?? new Set();
14
- signalListeners.add(listener);
15
- listeners.set(signal, signalListeners);
16
- ensureProcessHandler(signal);
17
- }
18
- export function unregisterSignalListener(signal, listener) {
19
- if (!isProcessAvailable()) {
20
- return;
21
- }
22
- const signalListeners = listeners.get(signal);
23
- if (!signalListeners) {
24
- return;
25
- }
26
- signalListeners.delete(listener);
27
- if (signalListeners.size === 0) {
28
- listeners.delete(signal);
29
- removeProcessHandler(signal);
30
- }
31
- }
32
- function ensureProcessHandler(signal) {
33
- if (processHandlers.has(signal)) {
34
- return;
35
- }
36
- const handler = () => {
37
- const signalListeners = listeners.get(signal);
38
- if (!signalListeners || signalListeners.size === 0) {
39
- return;
40
- }
41
- signalLogger.info({ signal, listenerCount: signalListeners.size }, 'Received process signal; notifying registered listeners');
42
- const listenersSnapshot = Array.from(signalListeners);
43
- void Promise.allSettled(listenersSnapshot.map(async (listener) => {
44
- try {
45
- await listener();
46
- }
47
- catch (error) {
48
- signalLogger.error({ signal, error: serializeError(error) }, 'Signal listener failed');
49
- }
50
- }));
51
- };
52
- process.on(signal, handler);
53
- processHandlers.set(signal, handler);
54
- }
55
- function removeProcessHandler(signal) {
56
- const handler = processHandlers.get(signal);
57
- if (!handler) {
58
- return;
59
- }
60
- if (typeof process.off === 'function') {
61
- process.off(signal, handler);
62
- }
63
- else if (typeof process.removeListener === 'function') {
64
- process.removeListener(signal, handler);
65
- }
66
- processHandlers.delete(signal);
67
- }