@looopy-ai/core 1.1.1 → 1.1.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.
@@ -6,6 +6,7 @@ import type { AnyEvent } from '../types/event';
6
6
  import type { LLMProvider } from '../types/llm';
7
7
  import type { Message } from '../types/message';
8
8
  import type { ToolProvider } from '../types/tools';
9
+ import type { SystemPromptProp } from '../utils/prompt';
9
10
  export interface AgentConfig {
10
11
  agentId: string;
11
12
  contextId: string;
@@ -15,7 +16,7 @@ export interface AgentConfig {
15
16
  agentStore?: AgentStore;
16
17
  autoCompact?: boolean;
17
18
  maxMessages?: number;
18
- systemPrompt?: string;
19
+ systemPrompt?: SystemPromptProp;
19
20
  logger?: import('pino').Logger;
20
21
  }
21
22
  export interface GetMessagesOptions {
@@ -14,7 +14,6 @@ export class Agent {
14
14
  this.config = {
15
15
  autoCompact: false,
16
16
  maxMessages: 100,
17
- systemPrompt: 'You are a helpful AI assistant.',
18
17
  ...config,
19
18
  logger: config.logger?.child({ contextId: config.contextId }) ||
20
19
  getLogger({ contextId: config.contextId }),
@@ -1,5 +1,6 @@
1
1
  import { concat, defer, filter, map, mergeMap, shareReplay } from 'rxjs';
2
2
  import { startLLMCallSpan, startLoopIterationSpan } from '../observability/spans';
3
+ import { getSystemPrompt } from '../utils/prompt';
3
4
  import { runToolCall } from './tools';
4
5
  export const runIteration = (context, config, history) => {
5
6
  const logger = context.logger.child({
@@ -8,11 +9,12 @@ export const runIteration = (context, config, history) => {
8
9
  });
9
10
  const { traceContext: iterationContext, tapFinish: finishIterationSpan } = startLoopIterationSpan({ ...context, logger }, config.iterationNumber);
10
11
  const llmEvents$ = defer(async () => {
11
- const messages = prepareMessages(context, history);
12
+ const systemPrompt = await getSystemPrompt(context.systemPrompt);
13
+ const messages = await prepareMessages(systemPrompt, context.skillPrompts, history);
12
14
  const tools = await prepareTools(context.toolProviders);
13
- return { messages, tools };
14
- }).pipe(mergeMap(({ messages, tools }) => {
15
- const { tapFinish: finishLLMCallSpan } = startLLMCallSpan({ ...context, parentContext: iterationContext }, messages);
15
+ return { messages, tools, systemPrompt };
16
+ }).pipe(mergeMap(({ messages, tools, systemPrompt }) => {
17
+ const { tapFinish: finishLLMCallSpan } = startLLMCallSpan({ ...context, parentContext: iterationContext }, systemPrompt, messages);
16
18
  return config.llmProvider
17
19
  .call({
18
20
  messages,
@@ -33,17 +35,17 @@ export const runIteration = (context, config, history) => {
33
35
  }, event)));
34
36
  return concat(llmEvents$.pipe(filter((event) => event.kind !== 'tool-call')), toolEvents$).pipe(finishIterationSpan);
35
37
  };
36
- const prepareMessages = (context, history) => {
38
+ const prepareMessages = async (systemPrompt, skillPrompts, history) => {
37
39
  const messages = [];
38
- if (context.systemPrompt) {
40
+ if (systemPrompt) {
39
41
  messages.push({
40
42
  role: 'system',
41
- content: context.systemPrompt,
42
- name: 'system-prompt',
43
+ content: systemPrompt.prompt,
44
+ name: systemPrompt.name || 'system-prompt',
43
45
  });
44
46
  }
45
- if (context.skillPrompts) {
46
- for (const [name, content] of Object.entries(context.skillPrompts)) {
47
+ if (skillPrompts) {
48
+ for (const [name, content] of Object.entries(skillPrompts)) {
47
49
  messages.push({
48
50
  role: 'system',
49
51
  content,
@@ -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({
@@ -17,7 +17,7 @@ export const runToolCall = (context, toolCall) => {
17
17
  return of(toolCall);
18
18
  }
19
19
  const { provider, tool } = matchingProvider;
20
- logger.trace({ providerName: provider.name }, 'Found tool provider for tool');
20
+ logger.debug({ providerName: provider.name, toolIcon: tool.icon }, 'Found tool provider for tool');
21
21
  const toolStartEvent = {
22
22
  kind: 'tool-start',
23
23
  contextId: context.contextId,
@@ -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));
@@ -1,13 +1,14 @@
1
1
  import type { LoopContext } from '../../core/types';
2
2
  import type { AnyEvent } from '../../types/event';
3
3
  import type { Message } from '../../types/message';
4
+ import type { SystemPrompt } from '../../utils/prompt';
4
5
  export interface LLMCallSpanParams {
5
6
  agentId: string;
6
7
  taskId: string;
7
8
  messages: Message[];
8
9
  parentContext: import('@opentelemetry/api').Context;
9
10
  }
10
- export declare const startLLMCallSpan: (context: LoopContext, messages: Message[]) => {
11
+ export declare const startLLMCallSpan: (context: LoopContext, systemPrompt: SystemPrompt | undefined, messages: Message[]) => {
11
12
  span: import("@opentelemetry/api").Span;
12
13
  traceContext: import("@opentelemetry/api").Context;
13
14
  tapFinish: import("rxjs").MonoTypeOperatorFunction<AnyEvent>;
@@ -1,7 +1,7 @@
1
1
  import { SpanStatusCode, trace } from '@opentelemetry/api';
2
2
  import { tap } from 'rxjs/internal/operators/tap';
3
3
  import { SpanAttributes, SpanNames } from '../tracing';
4
- export const startLLMCallSpan = (context, messages) => {
4
+ export const startLLMCallSpan = (context, systemPrompt, messages) => {
5
5
  const tracer = trace.getTracer('looopy');
6
6
  const span = tracer.startSpan(SpanNames.LLM_CALL, {
7
7
  attributes: {
@@ -9,6 +9,8 @@ export const startLLMCallSpan = (context, messages) => {
9
9
  [SpanAttributes.TASK_ID]: context.taskId,
10
10
  [SpanAttributes.GEN_AI_PROMPT]: JSON.stringify(messages),
11
11
  [SpanAttributes.LANGFUSE_OBSERVATION_TYPE]: 'generation',
12
+ [SpanAttributes.LANGFUSE_PROMPT_NAME]: systemPrompt?.name,
13
+ [SpanAttributes.LANGFUSE_PROMPT_VERSION]: systemPrompt?.version,
12
14
  },
13
15
  }, context.parentContext);
14
16
  const traceContext = trace.setSpan(context.parentContext, span);
@@ -33,6 +33,8 @@ export declare const SpanAttributes: {
33
33
  readonly LANGFUSE_METADATA: "langfuse.metadata";
34
34
  readonly LANGFUSE_VERSION: "langfuse.version";
35
35
  readonly LANGFUSE_RELEASE: "langfuse.release";
36
+ readonly LANGFUSE_PROMPT_NAME: "langfuse.prompt.name";
37
+ readonly LANGFUSE_PROMPT_VERSION: "langfuse.prompt.version";
36
38
  readonly GEN_AI_SYSTEM: "gen_ai.system";
37
39
  readonly GEN_AI_REQUEST_MODEL: "gen_ai.request.model";
38
40
  readonly GEN_AI_RESPONSE_MODEL: "gen_ai.response.model";
@@ -178,6 +178,8 @@ export const SpanAttributes = {
178
178
  LANGFUSE_METADATA: 'langfuse.metadata',
179
179
  LANGFUSE_VERSION: 'langfuse.version',
180
180
  LANGFUSE_RELEASE: 'langfuse.release',
181
+ LANGFUSE_PROMPT_NAME: 'langfuse.prompt.name',
182
+ LANGFUSE_PROMPT_VERSION: 'langfuse.prompt.version',
181
183
  GEN_AI_SYSTEM: 'gen_ai.system',
182
184
  GEN_AI_REQUEST_MODEL: 'gen_ai.request.model',
183
185
  GEN_AI_RESPONSE_MODEL: 'gen_ai.response.model',
@@ -107,14 +107,14 @@ export class LiteLLMProvider {
107
107
  ...usage,
108
108
  timestamp: new Date().toISOString(),
109
109
  })), this.debugLog('llm-usage'));
110
- const toolCalls$ = contentComplete$.pipe(mergeMap((event) => event.toolCalls?.map((tc) => ({
110
+ const toolCalls$ = contentComplete$.pipe(filter((event) => event.finishReason === 'tool_calls'), mergeMap((event) => event.toolCalls?.map((tc) => ({
111
111
  kind: 'tool-call',
112
112
  toolCallId: tc.id,
113
113
  toolName: tc.function.name,
114
114
  arguments: tc.function.arguments,
115
115
  timestamp: event.timestamp,
116
116
  })) || []));
117
- return merge(contentDeltas$, thoughts$, usageComplete$, toolCalls$).pipe(concatWith(contentComplete$));
117
+ return merge(contentDeltas$, thoughts$, usageComplete$).pipe(concatWith(toolCalls$), concatWith(contentComplete$));
118
118
  }
119
119
  debugLogRawChunk(chunk) {
120
120
  if (!this.config.debugLogPath) {
@@ -13,265 +13,335 @@ async function trackArtifactInState(taskId, artifactId, taskStateStore) {
13
13
  export function createArtifactTools(artifactStore, taskStateStore) {
14
14
  const scheduledStore = new ArtifactScheduler(artifactStore);
15
15
  return localTools([
16
- tool('create_file_artifact', 'Create a new file artifact for streaming text or binary content. Use append_file_chunk to add content. Set override=true to replace existing artifact.', z.object({
17
- artifactId: z
18
- .string()
19
- .describe('Unique identifier for the artifact (e.g., "report-2025", "analysis-results")'),
20
- name: z.string().optional().describe('Human-readable name for the artifact'),
21
- description: z.string().optional().describe('Description of the artifact content'),
22
- mimeType: z
23
- .string()
24
- .optional()
25
- .default('text/plain')
26
- .describe('MIME type of the content (e.g., "text/plain", "text/markdown")'),
27
- encoding: z
28
- .enum(['utf-8', 'base64'])
29
- .optional()
30
- .default('utf-8')
31
- .describe('Content encoding'),
32
- override: z
33
- .boolean()
34
- .optional()
35
- .default(false)
36
- .describe('Set to true to replace an existing artifact with the same ID'),
37
- }), async (params, context) => {
38
- await scheduledStore.createFileArtifact({
39
- artifactId: params.artifactId,
40
- taskId: context.taskId,
41
- contextId: context.contextId,
42
- name: params.name,
43
- description: params.description,
44
- mimeType: params.mimeType,
45
- encoding: params.encoding,
46
- override: params.override,
47
- });
48
- await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
49
- return {
50
- artifactId: params.artifactId,
51
- type: 'file',
52
- status: 'building',
53
- message: params.override
54
- ? 'File artifact reset. Use append_file_chunk to add content.'
55
- : 'File artifact created. Use append_file_chunk to add content.',
56
- };
16
+ tool({
17
+ name: 'create_file_artifact',
18
+ description: 'Create a new file artifact for streaming text or binary content. Use append_file_chunk to add content. Set override=true to replace existing artifact.',
19
+ schema: z.object({
20
+ artifactId: z
21
+ .string()
22
+ .describe('Unique identifier for the artifact (e.g., "report-2025", "analysis-results")'),
23
+ name: z.string().optional().describe('Human-readable name for the artifact'),
24
+ description: z.string().optional().describe('Description of the artifact content'),
25
+ mimeType: z
26
+ .string()
27
+ .optional()
28
+ .default('text/plain')
29
+ .describe('MIME type of the content (e.g., "text/plain", "text/markdown")'),
30
+ encoding: z
31
+ .enum(['utf-8', 'base64'])
32
+ .optional()
33
+ .default('utf-8')
34
+ .describe('Content encoding'),
35
+ override: z
36
+ .boolean()
37
+ .optional()
38
+ .default(false)
39
+ .describe('Set to true to replace an existing artifact with the same ID'),
40
+ }),
41
+ handler: async (params, context) => {
42
+ await scheduledStore.createFileArtifact({
43
+ artifactId: params.artifactId,
44
+ taskId: context.taskId,
45
+ contextId: context.contextId,
46
+ name: params.name,
47
+ description: params.description,
48
+ mimeType: params.mimeType,
49
+ encoding: params.encoding,
50
+ override: params.override,
51
+ });
52
+ await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
53
+ return {
54
+ artifactId: params.artifactId,
55
+ type: 'file',
56
+ status: 'building',
57
+ message: params.override
58
+ ? 'File artifact reset. Use append_file_chunk to add content.'
59
+ : 'File artifact created. Use append_file_chunk to add content.',
60
+ };
61
+ },
57
62
  }),
58
- tool('append_file_chunk', 'Append a chunk of content to a file artifact. Call multiple times to stream content.', z.object({
59
- artifactId: z.string().describe('The artifact ID to append to'),
60
- content_chunk: z.string().describe('Content chunk to append to the file'),
61
- isLastChunk: z
62
- .boolean()
63
- .optional()
64
- .default(false)
65
- .describe('Set to true on the final chunk to mark artifact as complete'),
66
- }), async (params, context) => {
67
- await scheduledStore.appendFileChunk(context.contextId, params.artifactId, params.content_chunk, {
68
- isLastChunk: params.isLastChunk,
69
- });
70
- return {
71
- artifactId: params.artifactId,
72
- chunkAdded: true,
73
- complete: params.isLastChunk,
74
- message: params.isLastChunk
75
- ? 'Final chunk appended. Artifact is complete.'
76
- : 'Chunk appended successfully.',
77
- };
63
+ tool({
64
+ name: 'append_file_chunk',
65
+ description: 'Append a chunk of content to a file artifact. Call multiple times to stream content.',
66
+ schema: z.object({
67
+ artifactId: z.string().describe('The artifact ID to append to'),
68
+ content_chunk: z.string().describe('Content chunk to append to the file'),
69
+ isLastChunk: z
70
+ .boolean()
71
+ .optional()
72
+ .default(false)
73
+ .describe('Set to true on the final chunk to mark artifact as complete'),
74
+ }),
75
+ handler: async (params, context) => {
76
+ await scheduledStore.appendFileChunk(context.contextId, params.artifactId, params.content_chunk, {
77
+ isLastChunk: params.isLastChunk,
78
+ });
79
+ return {
80
+ artifactId: params.artifactId,
81
+ chunkAdded: true,
82
+ complete: params.isLastChunk,
83
+ message: params.isLastChunk
84
+ ? 'Final chunk appended. Artifact is complete.'
85
+ : 'Chunk appended successfully.',
86
+ };
87
+ },
78
88
  }),
79
- tool('get_file_content', 'Get the complete content of a file artifact', z.object({
80
- artifactId: z.string().describe('The artifact ID to retrieve'),
81
- }), async (params, context) => {
82
- const content = await scheduledStore.getFileContent(context.contextId, params.artifactId);
83
- return {
84
- artifactId: params.artifactId,
85
- content,
86
- };
89
+ tool({
90
+ name: 'get_file_content',
91
+ description: 'Get the complete content of a file artifact',
92
+ schema: z.object({
93
+ artifactId: z.string().describe('The artifact ID to retrieve'),
94
+ }),
95
+ handler: async (params, context) => {
96
+ const content = await scheduledStore.getFileContent(context.contextId, params.artifactId);
97
+ return {
98
+ artifactId: params.artifactId,
99
+ content,
100
+ };
101
+ },
87
102
  }),
88
- tool('create_data_artifact', 'Create a data artifact with structured JSON data. Set override=true to replace existing artifact.', z.object({
89
- artifactId: z.string().describe('Unique identifier for the artifact'),
90
- name: z.string().optional().describe('Human-readable name'),
91
- description: z.string().optional().describe('Description of the data'),
92
- data: z.record(z.string(), z.unknown()).describe('The structured data object'),
93
- override: z
94
- .boolean()
95
- .optional()
96
- .default(false)
97
- .describe('Set to true to replace an existing artifact with the same ID'),
98
- }), async (params, context) => {
99
- await scheduledStore.createDataArtifact({
100
- artifactId: params.artifactId,
101
- taskId: context.taskId,
102
- contextId: context.contextId,
103
- name: params.name,
104
- description: params.description,
105
- override: params.override,
106
- });
107
- await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
108
- await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
109
- return {
110
- artifactId: params.artifactId,
111
- type: 'data',
112
- status: 'complete',
113
- message: params.override
114
- ? 'Data artifact reset successfully.'
115
- : 'Data artifact created successfully.',
116
- };
103
+ tool({
104
+ name: 'create_data_artifact',
105
+ description: 'Create a data artifact with structured JSON data. Set override=true to replace existing artifact.',
106
+ schema: z.object({
107
+ artifactId: z.string().describe('Unique identifier for the artifact'),
108
+ name: z.string().optional().describe('Human-readable name'),
109
+ description: z.string().optional().describe('Description of the data'),
110
+ data: z.record(z.string(), z.unknown()).describe('The structured data object'),
111
+ override: z
112
+ .boolean()
113
+ .optional()
114
+ .default(false)
115
+ .describe('Set to true to replace an existing artifact with the same ID'),
116
+ }),
117
+ handler: async (params, context) => {
118
+ await scheduledStore.createDataArtifact({
119
+ artifactId: params.artifactId,
120
+ taskId: context.taskId,
121
+ contextId: context.contextId,
122
+ name: params.name,
123
+ description: params.description,
124
+ override: params.override,
125
+ });
126
+ await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
127
+ await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
128
+ return {
129
+ artifactId: params.artifactId,
130
+ type: 'data',
131
+ status: 'complete',
132
+ message: params.override
133
+ ? 'Data artifact reset successfully.'
134
+ : 'Data artifact created successfully.',
135
+ };
136
+ },
117
137
  }),
118
- tool('update_data_artifact', 'Update the data content of an existing data artifact', z.object({
119
- artifactId: z.string().describe('The artifact ID to update'),
120
- data: z.record(z.string(), z.unknown()).describe('The new data object'),
121
- }), async (params, context) => {
122
- await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
123
- return {
124
- artifactId: params.artifactId,
125
- type: 'data',
126
- status: 'complete',
127
- message: 'Data artifact updated successfully.',
128
- };
138
+ tool({
139
+ name: 'update_data_artifact',
140
+ description: 'Update the data content of an existing data artifact',
141
+ schema: z.object({
142
+ artifactId: z.string().describe('The artifact ID to update'),
143
+ data: z.record(z.string(), z.unknown()).describe('The new data object'),
144
+ }),
145
+ handler: async (params, context) => {
146
+ await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
147
+ return {
148
+ artifactId: params.artifactId,
149
+ type: 'data',
150
+ status: 'complete',
151
+ message: 'Data artifact updated successfully.',
152
+ };
153
+ },
129
154
  }),
130
- tool('get_data_content', 'Get the content of a data artifact', z.object({
131
- artifactId: z.string().describe('The artifact ID to retrieve'),
132
- }), async (params, context) => {
133
- const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
134
- return {
135
- artifactId: params.artifactId,
136
- data,
137
- };
155
+ tool({
156
+ name: 'get_data_content',
157
+ description: 'Get the content of a data artifact',
158
+ schema: z.object({
159
+ artifactId: z.string().describe('The artifact ID to retrieve'),
160
+ }),
161
+ handler: async (params, context) => {
162
+ const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
163
+ return {
164
+ artifactId: params.artifactId,
165
+ data,
166
+ };
167
+ },
138
168
  }),
139
- tool('get_data_artifact', 'Get the data content of a data artifact', z.object({
140
- artifactId: z.string().describe('The artifact ID to retrieve'),
141
- }), async (params, context) => {
142
- const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
143
- return {
144
- artifactId: params.artifactId,
145
- data,
146
- };
169
+ tool({
170
+ name: 'get_data_artifact',
171
+ description: 'Get the data content of a data artifact',
172
+ schema: z.object({
173
+ artifactId: z.string().describe('The artifact ID to retrieve'),
174
+ }),
175
+ handler: async (params, context) => {
176
+ const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
177
+ return {
178
+ artifactId: params.artifactId,
179
+ data,
180
+ };
181
+ },
147
182
  }),
148
- tool('create_dataset_artifact', 'Create a dataset artifact for tabular data with a schema. Set override=true to replace existing artifact.', z.object({
149
- artifactId: z.string().describe('Unique identifier for the dataset'),
150
- name: z.string().optional().describe('Human-readable name'),
151
- description: z.string().optional().describe('Description of the dataset'),
183
+ tool({
184
+ name: 'create_dataset_artifact',
185
+ description: 'Create a dataset artifact for tabular data with a schema. Set override=true to replace existing artifact.',
152
186
  schema: z.object({
153
- columns: z.array(z.object({
154
- name: z.string(),
155
- type: z.enum(['string', 'number', 'boolean', 'date', 'json']),
156
- description: z.string().optional(),
157
- })),
187
+ artifactId: z.string().describe('Unique identifier for the dataset'),
188
+ name: z.string().optional().describe('Human-readable name'),
189
+ description: z.string().optional().describe('Description of the dataset'),
190
+ schema: z.object({
191
+ columns: z.array(z.object({
192
+ name: z.string(),
193
+ type: z.enum(['string', 'number', 'boolean', 'date', 'json']),
194
+ description: z.string().optional(),
195
+ })),
196
+ }),
197
+ override: z
198
+ .boolean()
199
+ .optional()
200
+ .default(false)
201
+ .describe('Set to true to replace an existing artifact with the same ID'),
158
202
  }),
159
- override: z
160
- .boolean()
161
- .optional()
162
- .default(false)
163
- .describe('Set to true to replace an existing artifact with the same ID'),
164
- }), async (params, context) => {
165
- await scheduledStore.createDatasetArtifact({
166
- artifactId: params.artifactId,
167
- taskId: context.taskId,
168
- contextId: context.contextId,
169
- name: params.name,
170
- description: params.description,
171
- schema: params.schema,
172
- override: params.override,
173
- });
174
- await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
175
- return {
176
- artifactId: params.artifactId,
177
- type: 'dataset',
178
- status: 'building',
179
- message: params.override
180
- ? 'Dataset artifact reset. Use append_dataset_row(s) to add data.'
181
- : 'Dataset artifact created. Use append_dataset_row(s) to add data.',
182
- };
203
+ handler: async (params, context) => {
204
+ await scheduledStore.createDatasetArtifact({
205
+ artifactId: params.artifactId,
206
+ taskId: context.taskId,
207
+ contextId: context.contextId,
208
+ name: params.name,
209
+ description: params.description,
210
+ schema: params.schema,
211
+ override: params.override,
212
+ });
213
+ await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
214
+ return {
215
+ artifactId: params.artifactId,
216
+ type: 'dataset',
217
+ status: 'building',
218
+ message: params.override
219
+ ? 'Dataset artifact reset. Use append_dataset_row(s) to add data.'
220
+ : 'Dataset artifact created. Use append_dataset_row(s) to add data.',
221
+ };
222
+ },
183
223
  }),
184
- tool('append_dataset_row', 'Append a single row to a dataset artifact', z.object({
185
- artifactId: z.string().describe('The dataset artifact ID'),
186
- row: z.record(z.string(), z.unknown()).describe('Row data matching the dataset schema'),
187
- }), async (params, context) => {
188
- await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, [params.row]);
189
- return {
190
- artifactId: params.artifactId,
191
- rowAdded: true,
192
- message: 'Row appended to dataset.',
193
- };
224
+ tool({
225
+ name: 'append_dataset_row',
226
+ description: 'Append a single row to a dataset artifact',
227
+ schema: z.object({
228
+ artifactId: z.string().describe('The dataset artifact ID'),
229
+ row: z.record(z.string(), z.unknown()).describe('Row data matching the dataset schema'),
230
+ }),
231
+ handler: async (params, context) => {
232
+ await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, [params.row]);
233
+ return {
234
+ artifactId: params.artifactId,
235
+ rowAdded: true,
236
+ message: 'Row appended to dataset.',
237
+ };
238
+ },
194
239
  }),
195
- tool('append_dataset_rows', 'Append multiple rows to a dataset artifact', z.object({
196
- artifactId: z.string().describe('The dataset artifact ID'),
197
- rows: z.array(z.record(z.string(), z.unknown())).describe('Array of rows to append'),
198
- isLastBatch: z.boolean().optional().describe('Set to true on the final batch'),
199
- }), async (params, context) => {
200
- await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, params.rows, {
201
- isLastBatch: params.isLastBatch,
202
- });
203
- return {
204
- artifactId: params.artifactId,
205
- rowsAdded: params.rows.length,
206
- message: `${params.rows.length} rows appended to dataset.`,
207
- };
240
+ tool({
241
+ name: 'append_dataset_rows',
242
+ description: 'Append multiple rows to a dataset artifact',
243
+ schema: z.object({
244
+ artifactId: z.string().describe('The dataset artifact ID'),
245
+ rows: z.array(z.record(z.string(), z.unknown())).describe('Array of rows to append'),
246
+ isLastBatch: z.boolean().optional().describe('Set to true on the final batch'),
247
+ }),
248
+ handler: async (params, context) => {
249
+ await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, params.rows, {
250
+ isLastBatch: params.isLastBatch,
251
+ });
252
+ return {
253
+ artifactId: params.artifactId,
254
+ rowsAdded: params.rows.length,
255
+ message: `${params.rows.length} rows appended to dataset.`,
256
+ };
257
+ },
208
258
  }),
209
- tool('get_dataset_rows', 'Get all rows from a dataset artifact', z.object({
210
- artifactId: z.string().describe('The dataset artifact ID'),
211
- }), async (params, context) => {
212
- const rows = await scheduledStore.getDatasetRows(context.contextId, params.artifactId);
213
- return {
214
- artifactId: params.artifactId,
215
- rows,
216
- totalRows: rows.length,
217
- };
259
+ tool({
260
+ name: 'get_dataset_rows',
261
+ description: 'Get all rows from a dataset artifact',
262
+ schema: z.object({
263
+ artifactId: z.string().describe('The dataset artifact ID'),
264
+ }),
265
+ handler: async (params, context) => {
266
+ const rows = await scheduledStore.getDatasetRows(context.contextId, params.artifactId);
267
+ return {
268
+ artifactId: params.artifactId,
269
+ rows,
270
+ totalRows: rows.length,
271
+ };
272
+ },
218
273
  }),
219
- tool('list_artifacts', 'List all artifacts in the current context, optionally filtered by task', z.object({
220
- taskId: z.string().optional().describe('Filter artifacts by task ID'),
221
- }), async (params, context) => {
222
- const artifactIds = await scheduledStore.listArtifacts(context.contextId, params.taskId);
223
- const artifacts = await Promise.all(artifactIds.map((id) => scheduledStore.getArtifact(context.contextId, id)));
224
- const validArtifacts = artifacts.filter((a) => a !== null);
225
- return {
226
- artifacts: validArtifacts.map((a) => ({
227
- artifactId: a.artifactId,
228
- type: a.type,
229
- name: a.name,
230
- taskId: a.taskId,
231
- contextId: a.contextId,
232
- createdAt: a.createdAt,
233
- })),
234
- totalCount: validArtifacts.length,
235
- };
274
+ tool({
275
+ name: 'list_artifacts',
276
+ description: 'List all artifacts in the current context, optionally filtered by task',
277
+ schema: z.object({
278
+ taskId: z.string().optional().describe('Filter artifacts by task ID'),
279
+ }),
280
+ handler: async (params, context) => {
281
+ const artifactIds = await scheduledStore.listArtifacts(context.contextId, params.taskId);
282
+ const artifacts = await Promise.all(artifactIds.map((id) => scheduledStore.getArtifact(context.contextId, id)));
283
+ const validArtifacts = artifacts.filter((a) => a !== null);
284
+ return {
285
+ artifacts: validArtifacts.map((a) => ({
286
+ artifactId: a.artifactId,
287
+ type: a.type,
288
+ name: a.name,
289
+ taskId: a.taskId,
290
+ contextId: a.contextId,
291
+ createdAt: a.createdAt,
292
+ })),
293
+ totalCount: validArtifacts.length,
294
+ };
295
+ },
236
296
  }),
237
- tool('get_artifact', 'Get metadata for a specific artifact by ID', z.object({
238
- artifactId: z.string().describe('The artifact ID to retrieve'),
239
- }), async (params, context) => {
240
- const artifact = await scheduledStore.getArtifact(context.contextId, params.artifactId);
241
- if (!artifact) {
242
- throw new Error(`Artifact not found: ${params.artifactId}`);
243
- }
244
- return {
245
- artifactId: artifact.artifactId,
246
- type: artifact.type,
247
- taskId: artifact.taskId,
248
- contextId: artifact.contextId,
249
- name: artifact.name,
250
- description: artifact.description,
251
- status: artifact.status,
252
- createdAt: artifact.createdAt,
253
- updatedAt: artifact.updatedAt,
254
- ...(artifact.type === 'file' && {
255
- mimeType: artifact.mimeType,
256
- encoding: artifact.encoding,
257
- totalChunks: artifact.totalChunks,
258
- totalSize: artifact.totalSize,
259
- }),
260
- ...(artifact.type === 'dataset' && {
261
- totalRows: artifact.totalSize,
262
- schema: artifact.schema,
263
- }),
264
- };
297
+ tool({
298
+ name: 'get_artifact',
299
+ description: 'Get metadata for a specific artifact by ID',
300
+ schema: z.object({
301
+ artifactId: z.string().describe('The artifact ID to retrieve'),
302
+ }),
303
+ handler: async (params, context) => {
304
+ const artifact = await scheduledStore.getArtifact(context.contextId, params.artifactId);
305
+ if (!artifact) {
306
+ throw new Error(`Artifact not found: ${params.artifactId}`);
307
+ }
308
+ return {
309
+ artifactId: artifact.artifactId,
310
+ type: artifact.type,
311
+ taskId: artifact.taskId,
312
+ contextId: artifact.contextId,
313
+ name: artifact.name,
314
+ description: artifact.description,
315
+ status: artifact.status,
316
+ createdAt: artifact.createdAt,
317
+ updatedAt: artifact.updatedAt,
318
+ ...(artifact.type === 'file' && {
319
+ mimeType: artifact.mimeType,
320
+ encoding: artifact.encoding,
321
+ totalChunks: artifact.totalChunks,
322
+ totalSize: artifact.totalSize,
323
+ }),
324
+ ...(artifact.type === 'dataset' && {
325
+ totalRows: artifact.totalSize,
326
+ schema: artifact.schema,
327
+ }),
328
+ };
329
+ },
265
330
  }),
266
- tool('delete_artifact', 'Delete an artifact by ID', z.object({
267
- artifactId: z.string().describe('The artifact ID to delete'),
268
- }), async (params, context) => {
269
- await scheduledStore.deleteArtifact(context.contextId, params.artifactId);
270
- return {
271
- artifactId: params.artifactId,
272
- deleted: true,
273
- message: 'Artifact deleted successfully.',
274
- };
331
+ tool({
332
+ name: 'delete_artifact',
333
+ description: 'Delete an artifact by ID',
334
+ schema: z.object({
335
+ artifactId: z.string().describe('The artifact ID to delete'),
336
+ }),
337
+ handler: async (params, context) => {
338
+ await scheduledStore.deleteArtifact(context.contextId, params.artifactId);
339
+ return {
340
+ artifactId: params.artifactId,
341
+ deleted: true,
342
+ message: 'Artifact deleted successfully.',
343
+ };
344
+ },
275
345
  }),
276
346
  ]);
277
347
  }
@@ -9,5 +9,5 @@ export interface LocalToolDefinition<TSchema extends z.ZodObject> {
9
9
  schema: TSchema;
10
10
  handler: ToolHandler<z.infer<TSchema>>;
11
11
  }
12
- export declare function tool<TSchema extends z.ZodObject>(name: string, description: string, schema: TSchema, handler: ToolHandler<z.infer<TSchema>>): LocalToolDefinition<TSchema>;
12
+ export declare function tool<TSchema extends z.ZodObject>(definition: LocalToolDefinition<TSchema>): LocalToolDefinition<TSchema>;
13
13
  export declare function localTools(tools: LocalToolDefinition<z.ZodObject>[]): ToolProvider;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- export function tool(name, description, schema, handler) {
3
- return { name, description, schema, handler };
2
+ export function tool(definition) {
3
+ return { ...definition };
4
4
  }
5
5
  const zodToJsonSchema = (schema) => {
6
6
  const fullSchema = z.toJSONSchema(schema);
@@ -0,0 +1,7 @@
1
+ export type SystemPrompt = {
2
+ prompt: string;
3
+ name?: string;
4
+ version?: string;
5
+ };
6
+ export type SystemPromptProp = string | SystemPrompt | (() => Promise<SystemPrompt> | SystemPrompt);
7
+ export declare const getSystemPrompt: (systemPrompt?: SystemPromptProp) => Promise<SystemPrompt | undefined>;
@@ -0,0 +1,12 @@
1
+ export const getSystemPrompt = async (systemPrompt) => {
2
+ if (!systemPrompt) {
3
+ return undefined;
4
+ }
5
+ if (typeof systemPrompt === 'string') {
6
+ return { prompt: systemPrompt };
7
+ }
8
+ if (typeof systemPrompt === 'function') {
9
+ return await systemPrompt();
10
+ }
11
+ return systemPrompt;
12
+ };
@@ -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.1",
3
+ "version": "1.1.3",
4
4
  "description": "RxJS-based AI agent framework",
5
5
  "keywords": [
6
6
  "agent",