@looopy-ai/core 1.0.4 → 1.1.1

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
  }
@@ -31,7 +31,7 @@ export const runIteration = (context, config, history) => {
31
31
  logger: context.logger.child({ iteration: config.iterationNumber }),
32
32
  parentContext: iterationContext,
33
33
  }, event)));
34
- return concat(llmEvents$, toolEvents$).pipe(finishIterationSpan);
34
+ return concat(llmEvents$.pipe(filter((event) => event.kind !== 'tool-call')), toolEvents$).pipe(finishIterationSpan);
35
35
  };
36
36
  const prepareMessages = (context, history) => {
37
37
  const messages = [];
@@ -1,4 +1,4 @@
1
- import { concat, defer, of } from 'rxjs';
1
+ import { concat, defer, mergeMap, of } from 'rxjs';
2
2
  import { startToolExecuteSpan } from '../observability/spans';
3
3
  export const runToolCall = (context, toolCall) => {
4
4
  const logger = context.logger.child({
@@ -6,54 +6,56 @@ export const runToolCall = (context, toolCall) => {
6
6
  toolCallId: toolCall.toolCallId,
7
7
  toolName: toolCall.toolName,
8
8
  });
9
- const toolStartEvent = {
10
- kind: 'tool-start',
11
- contextId: context.contextId,
12
- taskId: context.taskId,
13
- toolCallId: toolCall.toolCallId,
14
- toolName: toolCall.toolName,
15
- arguments: toolCall.arguments,
16
- timestamp: new Date().toISOString(),
17
- };
18
- const { tapFinish } = startToolExecuteSpan(context, toolStartEvent);
19
- const toolResultEvents$ = defer(async () => {
20
- logger.trace('Executing tool');
21
- const provider = context.toolProviders.find((p) => p.canHandle(toolCall.toolName));
22
- if (!provider) {
23
- logger.warn('No provider found for tool');
24
- const errorMessage = `No provider found for tool: ${toolCall.toolName}`;
25
- return createToolErrorEvent(context, toolCall, errorMessage);
26
- }
27
- try {
28
- const result = await provider.execute({
29
- id: toolCall.toolCallId,
30
- type: 'function',
31
- function: {
32
- name: toolCall.toolName,
33
- arguments: toolCall.arguments,
34
- },
35
- }, {
36
- contextId: context.contextId,
37
- taskId: context.taskId,
38
- agentId: context.agentId,
39
- parentContext: context.parentContext,
40
- authContext: context.authContext,
41
- });
42
- logger.trace({
43
- success: result.success,
44
- }, 'Tool execution complete');
45
- return createToolCompleteEvent(context, toolCall, result.result);
9
+ return defer(async () => {
10
+ const matchingProviders = await Promise.all(context.toolProviders.map(async (p) => ({
11
+ provider: p,
12
+ tool: await p.getTool(toolCall.toolName),
13
+ })));
14
+ const matchingProvider = matchingProviders.find((p) => p.tool !== undefined);
15
+ if (!matchingProvider) {
16
+ logger.warn('No tool provider found for tool');
17
+ return of(toolCall);
46
18
  }
47
- catch (error) {
48
- const err = error instanceof Error ? error : new Error(String(error));
49
- logger.error({
50
- error: err.message,
51
- stack: err.stack,
52
- }, 'Tool execution failed');
53
- return createToolErrorEvent(context, toolCall, err.message);
54
- }
55
- });
56
- return concat(of(toolStartEvent), toolResultEvents$).pipe(tapFinish);
19
+ const { provider, tool } = matchingProvider;
20
+ logger.trace({ providerName: provider.name }, 'Found tool provider for tool');
21
+ const toolStartEvent = {
22
+ kind: 'tool-start',
23
+ contextId: context.contextId,
24
+ taskId: context.taskId,
25
+ toolCallId: toolCall.toolCallId,
26
+ icon: tool.icon,
27
+ toolName: toolCall.toolName,
28
+ arguments: toolCall.arguments,
29
+ timestamp: new Date().toISOString(),
30
+ };
31
+ const { tapFinish } = startToolExecuteSpan(context, toolCall);
32
+ const toolResultEvents$ = defer(async () => {
33
+ logger.trace({ providerName: provider.name }, 'Executing tool');
34
+ try {
35
+ const result = await provider.execute({
36
+ id: toolCall.toolCallId,
37
+ type: 'function',
38
+ function: {
39
+ name: toolCall.toolName,
40
+ arguments: toolCall.arguments,
41
+ },
42
+ }, context);
43
+ logger.trace({
44
+ success: result.success,
45
+ }, 'Tool execution complete');
46
+ return createToolCompleteEvent(context, toolCall, result.result);
47
+ }
48
+ catch (error) {
49
+ const err = error instanceof Error ? error : new Error(String(error));
50
+ logger.error({
51
+ error: err.message,
52
+ stack: err.stack,
53
+ }, 'Tool execution failed');
54
+ return createToolErrorEvent(context, toolCall, err.message);
55
+ }
56
+ });
57
+ return concat(of(toolStartEvent), toolResultEvents$).pipe(tapFinish);
58
+ }).pipe(mergeMap((obs) => obs));
57
59
  };
58
60
  const createToolCompleteEvent = (context, toolCall, result) => ({
59
61
  kind: 'tool-complete',
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,5 +1,5 @@
1
1
  import type { IterationContext } from '../../core/types';
2
- import type { ToolExecutionEvent, ToolStartEvent } from '../../types/event';
2
+ import type { ToolCallEvent, ToolExecutionEvent } from '../../types/event';
3
3
  import type { ToolCall } from '../../types/tools';
4
4
  export interface ToolExecutionSpanParams {
5
5
  agentId: string;
@@ -7,7 +7,7 @@ export interface ToolExecutionSpanParams {
7
7
  toolCall: ToolCall;
8
8
  parentContext?: import('@opentelemetry/api').Context;
9
9
  }
10
- export declare const startToolExecuteSpan: (context: IterationContext, toolStart: ToolStartEvent) => {
10
+ export declare const startToolExecuteSpan: (context: IterationContext, toolStart: ToolCallEvent) => {
11
11
  span: import("@opentelemetry/api").Span;
12
12
  traceContext: import("@opentelemetry/api").Context;
13
13
  tapFinish: import("rxjs").MonoTypeOperatorFunction<ToolExecutionEvent>;
@@ -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
+ }
@@ -5,18 +5,18 @@ export interface ClientToolConfig {
5
5
  onInputRequired: (toolCall: ToolCall, context: ExecutionContext) => Promise<ToolResult>;
6
6
  }
7
7
  export declare class ClientToolProvider implements ToolProvider {
8
+ name: string;
8
9
  private readonly tools;
9
10
  private readonly toolNames;
10
11
  private readonly onInputRequired;
11
12
  constructor(config: ClientToolConfig);
12
13
  getTools(): Promise<ToolDefinition[]>;
13
- canHandle(toolName: string): boolean;
14
14
  execute(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult>;
15
- getTool(name: string): ToolDefinition | undefined;
16
- validateToolArguments(toolCall: ToolCall): {
15
+ getTool(name: string): Promise<ToolDefinition | undefined>;
16
+ validateToolArguments(toolCall: ToolCall): Promise<{
17
17
  valid: boolean;
18
18
  errors?: string[];
19
- };
19
+ }>;
20
20
  private validateRequiredParams;
21
21
  private validateUnknownParams;
22
22
  private validateParamTypes;
@@ -1,5 +1,6 @@
1
1
  import { validateToolDefinitions, } from '../types/tools';
2
2
  export class ClientToolProvider {
3
+ name = 'client-tool-provider';
3
4
  tools;
4
5
  toolNames;
5
6
  onInputRequired;
@@ -24,11 +25,9 @@ export class ClientToolProvider {
24
25
  async getTools() {
25
26
  return [...this.tools];
26
27
  }
27
- canHandle(toolName) {
28
- return this.toolNames.has(toolName);
29
- }
30
28
  async execute(toolCall, context) {
31
- if (!this.canHandle(toolCall.function.name)) {
29
+ const tool = await this.getTool(toolCall.function.name);
30
+ if (!tool) {
32
31
  return {
33
32
  toolCallId: toolCall.id,
34
33
  toolName: toolCall.function.name,
@@ -60,11 +59,11 @@ export class ClientToolProvider {
60
59
  };
61
60
  }
62
61
  }
63
- getTool(name) {
62
+ async getTool(name) {
64
63
  return this.tools.find((t) => t.name === name);
65
64
  }
66
- validateToolArguments(toolCall) {
67
- const tool = this.getTool(toolCall.function.name);
65
+ async validateToolArguments(toolCall) {
66
+ const tool = await this.getTool(toolCall.function.name);
68
67
  if (!tool) {
69
68
  return { valid: false, errors: [`Tool ${toolCall.function.name} not found`] };
70
69
  }
@@ -5,6 +5,7 @@ export type ToolHandler<TParams> = (params: TParams, context: ExecutionContext)
5
5
  export interface LocalToolDefinition<TSchema extends z.ZodObject> {
6
6
  name: string;
7
7
  description: string;
8
+ icon?: string;
8
9
  schema: TSchema;
9
10
  handler: ToolHandler<z.infer<TSchema>>;
10
11
  }
@@ -19,12 +19,25 @@ export function localTools(tools) {
19
19
  toolMap.set(tool.name, tool);
20
20
  }
21
21
  return {
22
+ name: 'local-tool-provider',
22
23
  getTools: async () => tools.map((t) => ({
23
24
  name: t.name,
24
25
  description: t.description,
26
+ icon: t.icon,
25
27
  parameters: zodToJsonSchema(t.schema),
26
28
  })),
27
- canHandle: (toolName) => toolMap.has(toolName),
29
+ getTool: async (toolName) => {
30
+ const toolDef = toolMap.get(toolName);
31
+ if (!toolDef) {
32
+ return undefined;
33
+ }
34
+ return {
35
+ name: toolDef.name,
36
+ description: toolDef.description,
37
+ icon: toolDef.icon,
38
+ parameters: zodToJsonSchema(toolDef.schema),
39
+ };
40
+ },
28
41
  execute: async (toolCall, context) => {
29
42
  const toolDef = toolMap.get(toolCall.function.name);
30
43
  if (!toolDef) {
@@ -8,6 +8,7 @@ export interface MCPProviderConfig {
8
8
  }
9
9
  export declare const mcp: (config: MCPProviderConfig) => McpToolProvider;
10
10
  export declare class McpToolProvider implements ToolProvider {
11
+ name: string;
11
12
  readonly id: string;
12
13
  private readonly client;
13
14
  private toolCache;
@@ -15,8 +16,9 @@ export declare class McpToolProvider implements ToolProvider {
15
16
  private readonly cacheTTL;
16
17
  private ongoingRequest;
17
18
  constructor(config: MCPProviderConfig);
19
+ getTool(toolName: string): Promise<ToolDefinition | undefined>;
20
+ executeBatch(toolCalls: ToolCall[], context: ExecutionContext): Promise<ToolResult[]>;
18
21
  getTools(): Promise<ToolDefinition[]>;
19
- canHandle(toolName: string): boolean;
20
22
  execute(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult>;
21
23
  private convertMCPTool;
22
24
  }
@@ -3,6 +3,7 @@ export const mcp = (config) => {
3
3
  return new McpToolProvider(config);
4
4
  };
5
5
  export class McpToolProvider {
6
+ name = 'mcp-tool-provider';
6
7
  id;
7
8
  client;
8
9
  toolCache = new Map();
@@ -17,6 +18,13 @@ export class McpToolProvider {
17
18
  getHeaders: config.getHeaders,
18
19
  });
19
20
  }
21
+ async getTool(toolName) {
22
+ const tools = await this.getTools();
23
+ return tools.find((tool) => tool.name === toolName);
24
+ }
25
+ async executeBatch(toolCalls, context) {
26
+ return Promise.all(toolCalls.map((call) => this.execute(call, context)));
27
+ }
20
28
  async getTools() {
21
29
  if (this.toolCache.size > 0 && this.cacheExpiry && Date.now() < this.cacheExpiry) {
22
30
  return Array.from(this.toolCache.values());
@@ -40,9 +48,6 @@ export class McpToolProvider {
40
48
  });
41
49
  return this.ongoingRequest;
42
50
  }
43
- canHandle(toolName) {
44
- return this.toolCache.has(toolName);
45
- }
46
51
  async execute(toolCall, context) {
47
52
  const { name, arguments: args } = toolCall.function;
48
53
  if (typeof args !== 'object' || args === null) {
@@ -98,6 +98,7 @@ export interface ToolStartEvent {
98
98
  contextId: string;
99
99
  taskId: string;
100
100
  toolCallId: string;
101
+ icon?: string;
101
102
  toolName: string;
102
103
  arguments: Record<string, unknown>;
103
104
  timestamp: string;
@@ -112,6 +113,7 @@ export interface ToolProgressEvent {
112
113
  contextId: string;
113
114
  taskId: string;
114
115
  toolCallId: string;
116
+ icon?: string;
115
117
  progress: number;
116
118
  message?: string;
117
119
  timestamp: string;
@@ -127,6 +129,7 @@ export interface ToolCompleteEvent {
127
129
  contextId: string;
128
130
  taskId: string;
129
131
  toolCallId: string;
132
+ icon?: string;
130
133
  toolName: string;
131
134
  success: boolean;
132
135
  result?: unknown;
@@ -27,6 +27,7 @@ export type FunctionParameters = z.infer<typeof FunctionParametersSchema>;
27
27
  export declare const ToolDefinitionSchema: z.ZodObject<{
28
28
  name: z.ZodString;
29
29
  description: z.ZodString;
30
+ icon: z.ZodOptional<z.ZodString>;
30
31
  parameters: z.ZodObject<{
31
32
  type: z.ZodLiteral<"object">;
32
33
  properties: z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
@@ -50,8 +51,9 @@ export declare const ToolCallSchema: z.ZodObject<{
50
51
  }, z.core.$strip>;
51
52
  }, z.core.$strip>;
52
53
  export type ToolProvider = {
54
+ get name(): string;
55
+ getTool(toolName: string): Promise<ToolDefinition | undefined>;
53
56
  getTools(): Promise<ToolDefinition[]>;
54
57
  execute(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult>;
55
- canHandle(toolName: string): boolean;
56
58
  executeBatch?(toolCalls: ToolCall[], context: ExecutionContext): Promise<ToolResult[]>;
57
59
  };
@@ -31,6 +31,7 @@ export const ToolDefinitionSchema = z.object({
31
31
  .max(64)
32
32
  .regex(/^[a-zA-Z0-9_-]+$/, 'Tool name must contain only alphanumeric characters, underscores, and hyphens'),
33
33
  description: z.string().min(1).max(1024),
34
+ icon: z.string().optional(),
34
35
  parameters: FunctionParametersSchema,
35
36
  });
36
37
  export function validateToolDefinitions(tools) {
@@ -0,0 +1 @@
1
+ export * from './error';
@@ -0,0 +1 @@
1
+ export * from './error';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looopy-ai/core",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
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
- }