smoltalk 0.0.35 → 0.0.36

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.
@@ -1,6 +1,8 @@
1
+ import { StatelogClient } from "../statelogClient.js";
1
2
  import { PromptConfig, PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
2
3
  export declare class BaseClient implements SmolClient {
3
4
  protected config: SmolConfig;
5
+ protected statelogClient?: StatelogClient;
4
6
  constructor(config: SmolConfig);
5
7
  text(promptConfig: Omit<PromptConfig, "stream">): Promise<Result<PromptResult>>;
6
8
  text(promptConfig: Omit<PromptConfig, "stream"> & {
@@ -1,12 +1,17 @@
1
1
  import { userMessage, assistantMessage } from "../classes/message/index.js";
2
2
  import { getLogger } from "../logger.js";
3
+ import { getStatelogClient } from "../statelogClient.js";
3
4
  import { success, } from "../types.js";
4
5
  import { z } from "zod";
5
6
  const DEFAULT_NUM_RETRIES = 2;
6
7
  export class BaseClient {
7
8
  config;
9
+ statelogClient;
8
10
  constructor(config) {
9
11
  this.config = config || {};
12
+ if (this.config.statelog) {
13
+ this.statelogClient = getStatelogClient(this.config.statelog);
14
+ }
10
15
  }
11
16
  text(promptConfig) {
12
17
  if (promptConfig.stream) {
@@ -39,7 +44,8 @@ export class BaseClient {
39
44
  value: { output: null, toolCalls: [], model: this.config.model },
40
45
  };
41
46
  }
42
- return this.textWithRetry(newPromptConfig, newPromptConfig.responseFormatOptions?.numRetries || DEFAULT_NUM_RETRIES);
47
+ const result = await this.textWithRetry(newPromptConfig, newPromptConfig.responseFormatOptions?.numRetries || DEFAULT_NUM_RETRIES);
48
+ return result;
43
49
  }
44
50
  checkForToolLoops(promptConfig) {
45
51
  if (!this.config.toolLoopDetection?.enabled) {
@@ -57,6 +63,11 @@ export class BaseClient {
57
63
  const intervention = this.config.toolLoopDetection.intervention || "remove-tool";
58
64
  const logger = getLogger();
59
65
  logger.warn(`Tool loop detected for tool "${toolName}" called ${count} times. Intervention: ${intervention}`);
66
+ this.statelogClient?.debug("Tool loop detected", {
67
+ toolName,
68
+ count,
69
+ intervention,
70
+ });
60
71
  switch (intervention) {
61
72
  case "remove-tool":
62
73
  const newTools = promptConfig.tools?.filter((t) => t.name !== toolName);
@@ -167,10 +178,15 @@ export class BaseClient {
167
178
  catch (err) {
168
179
  const errorMessage = err.message;
169
180
  const logger = getLogger();
170
- logger.debug(`Response format validation failed (retries left: ${retries}): `, errorMessage, "output:", JSON.stringify(output, null, 2), "responseFormat:", JSON.stringify(promptConfig.responseFormat, null, 2));
181
+ logger.warn(`Response format validation failed (retries left: ${retries}): `, errorMessage, "output:", JSON.stringify(output, null, 2), "responseFormat:", JSON.stringify(promptConfig.responseFormat, null, 2));
171
182
  if (err instanceof z.ZodError) {
172
- logger.debug("Zod error details:", z.prettifyError(err));
183
+ logger.warn("Zod error details:", z.prettifyError(err));
173
184
  }
185
+ this.statelogClient?.diff({
186
+ message: "Response format validation failed",
187
+ itemA: promptConfig.responseFormat,
188
+ itemB: output,
189
+ });
174
190
  const retryMessages = [
175
191
  ...promptConfig.messages,
176
192
  assistantMessage(output),
package/dist/functions.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getClient } from "./client.js";
2
2
  import { isModelConfig, pickModel } from "./models.js";
3
3
  function splitConfig(config) {
4
- const { openAiApiKey, googleApiKey, ollamaApiKey, anthropicApiKey, ollamaHost, model: rawModel, provider, logLevel, toolLoopDetection, ...promptConfig } = config;
4
+ const { openAiApiKey, googleApiKey, ollamaApiKey, anthropicApiKey, ollamaHost, model: rawModel, provider, logLevel, toolLoopDetection, statelog, ...promptConfig } = config;
5
5
  const model = isModelConfig(rawModel) ? pickModel(rawModel) : rawModel;
6
6
  return {
7
7
  smolConfig: {
@@ -14,6 +14,7 @@ function splitConfig(config) {
14
14
  provider,
15
15
  logLevel,
16
16
  toolLoopDetection,
17
+ statelog,
17
18
  },
18
19
  promptConfig,
19
20
  };
@@ -0,0 +1,113 @@
1
+ import { JSONEdge } from "./types.js";
2
+ import { Result } from "./types/result.js";
3
+ import { ModelConfig, ModelName } from "./models.js";
4
+ export type AgencyFile = {
5
+ name: string;
6
+ contents: string;
7
+ };
8
+ export type UploadResult = Result<{
9
+ endpointUrls: string[];
10
+ }>;
11
+ export type StatelogConfig = {
12
+ host: string;
13
+ traceId?: string;
14
+ apiKey: string;
15
+ projectId: string;
16
+ debugMode: boolean;
17
+ };
18
+ export declare function mergeUploadResults(_results: UploadResult[]): UploadResult;
19
+ export declare class StatelogClient {
20
+ private host;
21
+ private debugMode;
22
+ private traceId;
23
+ private apiKey;
24
+ private projectId;
25
+ constructor(config: StatelogConfig);
26
+ toJSON(): {
27
+ traceId: string;
28
+ projectId: string;
29
+ host: string;
30
+ debugMode: boolean;
31
+ };
32
+ debug(message: string, data: any): Promise<void>;
33
+ graph({ nodes, edges, startNode, }: {
34
+ nodes: string[];
35
+ edges: Record<string, JSONEdge>;
36
+ startNode?: string;
37
+ }): Promise<void>;
38
+ enterNode({ nodeId, data, }: {
39
+ nodeId: string;
40
+ data: any;
41
+ }): Promise<void>;
42
+ exitNode({ nodeId, data, timeTaken, }: {
43
+ nodeId: string;
44
+ data: any;
45
+ timeTaken?: number;
46
+ }): Promise<void>;
47
+ beforeHook({ nodeId, startData, endData, timeTaken, }: {
48
+ nodeId: string;
49
+ startData: any;
50
+ endData: any;
51
+ timeTaken?: number;
52
+ }): Promise<void>;
53
+ afterHook({ nodeId, startData, endData, timeTaken, }: {
54
+ nodeId: string;
55
+ startData: any;
56
+ endData: any;
57
+ timeTaken?: number;
58
+ }): Promise<void>;
59
+ followEdge({ fromNodeId, toNodeId, isConditionalEdge, data, }: {
60
+ fromNodeId: string;
61
+ toNodeId: string;
62
+ isConditionalEdge: boolean;
63
+ data: any;
64
+ }): Promise<void>;
65
+ promptCompletion({ messages, completion, model, timeTaken, tools, responseFormat, }: {
66
+ messages: any[];
67
+ completion: any;
68
+ model?: ModelName | ModelConfig | string;
69
+ timeTaken?: number;
70
+ tools?: {
71
+ name: string;
72
+ description?: string;
73
+ schema: any;
74
+ }[];
75
+ responseFormat?: any;
76
+ }): Promise<void>;
77
+ toolCall({ toolName, args, output, model, timeTaken, }: {
78
+ toolName: string;
79
+ args: any;
80
+ output: any;
81
+ model?: ModelName | ModelConfig;
82
+ timeTaken?: number;
83
+ }): Promise<void>;
84
+ diff({ itemA, itemB, message, }: {
85
+ itemA: any;
86
+ itemB: any;
87
+ message?: string;
88
+ }): Promise<void>;
89
+ upload({ projectId, entrypoint, files, }: {
90
+ projectId: string;
91
+ entrypoint: string;
92
+ files: AgencyFile[];
93
+ }): Promise<UploadResult>;
94
+ remoteRun({ files, entrypoint, args, }: {
95
+ files: AgencyFile[];
96
+ entrypoint: string;
97
+ args?: any[];
98
+ }): Promise<Result<any>>;
99
+ hitServer({ userId, projectId, filename, nodeName, body, }: {
100
+ userId: string;
101
+ projectId: string;
102
+ filename: string;
103
+ nodeName: string;
104
+ body: string;
105
+ }): Promise<Result<any>>;
106
+ post(body: Record<string, any>): Promise<void>;
107
+ }
108
+ export declare function getStatelogClient(config: {
109
+ host: string;
110
+ traceId?: string;
111
+ projectId: string;
112
+ debugMode?: boolean;
113
+ }): StatelogClient;
@@ -0,0 +1,303 @@
1
+ import { nanoid } from "nanoid";
2
+ import { failure, mergeResults, success } from "./types/result.js";
3
+ export function mergeUploadResults(_results) {
4
+ const results = mergeResults(_results);
5
+ if (!results.success) {
6
+ return failure(results.error);
7
+ }
8
+ const endpointUrls = results.value.flatMap((r) => r.endpointUrls);
9
+ return success({
10
+ endpointUrls,
11
+ });
12
+ }
13
+ export class StatelogClient {
14
+ host;
15
+ debugMode;
16
+ traceId;
17
+ apiKey;
18
+ projectId;
19
+ constructor(config) {
20
+ const { host, apiKey, projectId, traceId, debugMode } = config;
21
+ this.host = host;
22
+ this.apiKey = apiKey;
23
+ this.projectId = projectId;
24
+ this.debugMode = debugMode || false;
25
+ this.traceId = traceId || nanoid();
26
+ if (this.debugMode) {
27
+ console.log(`Statelog client initialized with host: ${host} and traceId: ${this.traceId}`, { config });
28
+ }
29
+ if (!this.apiKey) {
30
+ throw new Error("API key is required for StatelogClient");
31
+ }
32
+ }
33
+ toJSON() {
34
+ return {
35
+ traceId: this.traceId,
36
+ projectId: this.projectId,
37
+ host: this.host,
38
+ debugMode: this.debugMode,
39
+ };
40
+ }
41
+ async debug(message, data) {
42
+ await this.post({
43
+ type: "debug",
44
+ message: message,
45
+ data,
46
+ });
47
+ }
48
+ async graph({ nodes, edges, startNode, }) {
49
+ await this.post({
50
+ type: "graph",
51
+ nodes,
52
+ edges,
53
+ startNode,
54
+ });
55
+ }
56
+ async enterNode({ nodeId, data, }) {
57
+ await this.post({
58
+ type: "enterNode",
59
+ nodeId,
60
+ data,
61
+ });
62
+ }
63
+ async exitNode({ nodeId, data, timeTaken, }) {
64
+ await this.post({
65
+ type: "exitNode",
66
+ nodeId,
67
+ data,
68
+ timeTaken,
69
+ });
70
+ }
71
+ async beforeHook({ nodeId, startData, endData, timeTaken, }) {
72
+ await this.post({
73
+ type: "beforeHook",
74
+ nodeId,
75
+ startData,
76
+ endData,
77
+ timeTaken,
78
+ });
79
+ }
80
+ async afterHook({ nodeId, startData, endData, timeTaken, }) {
81
+ await this.post({
82
+ type: "afterHook",
83
+ nodeId,
84
+ startData,
85
+ endData,
86
+ timeTaken,
87
+ });
88
+ }
89
+ async followEdge({ fromNodeId, toNodeId, isConditionalEdge, data, }) {
90
+ await this.post({
91
+ type: "followEdge",
92
+ edgeId: `${fromNodeId}->${toNodeId}`,
93
+ fromNodeId,
94
+ toNodeId,
95
+ isConditionalEdge,
96
+ data,
97
+ });
98
+ }
99
+ async promptCompletion({ messages, completion, model, timeTaken, tools, responseFormat, }) {
100
+ await this.post({
101
+ type: "promptCompletion",
102
+ messages,
103
+ completion,
104
+ model,
105
+ timeTaken,
106
+ tools,
107
+ responseFormat,
108
+ });
109
+ }
110
+ async toolCall({ toolName, args, output, model, timeTaken, }) {
111
+ await this.post({
112
+ type: "toolCall",
113
+ toolName,
114
+ args,
115
+ output,
116
+ model,
117
+ timeTaken,
118
+ });
119
+ }
120
+ async diff({ itemA, itemB, message, }) {
121
+ await this.post({
122
+ type: "diff",
123
+ itemA,
124
+ itemB,
125
+ message,
126
+ });
127
+ }
128
+ /* async promptResult({ result }: { result: PromptResult }): Promise<void> {
129
+ await this.post({
130
+ type: "promptResult",
131
+ result,
132
+ });
133
+ }
134
+ */
135
+ async upload({ projectId, entrypoint, files, }) {
136
+ try {
137
+ const fullUrl = new URL(`/api/projects/${projectId}/upload`, this.host);
138
+ const url = fullUrl.toString();
139
+ const postBody = JSON.stringify({ entrypoint, files });
140
+ console.log({ entrypoint, files }, postBody);
141
+ const result = await fetch(url, {
142
+ method: "POST",
143
+ headers: {
144
+ "Content-Type": "application/json",
145
+ Authorization: `Bearer ${this.apiKey}`,
146
+ },
147
+ body: postBody,
148
+ }).catch((err) => {
149
+ if (this.debugMode)
150
+ console.error("Failed to send statelog:", err);
151
+ });
152
+ if (result) {
153
+ if (!result.ok) {
154
+ if (this.debugMode)
155
+ console.error("Failed to upload files to statelog:", {
156
+ result,
157
+ url,
158
+ files,
159
+ });
160
+ return failure("Failed to upload files to statelog");
161
+ }
162
+ return (await result.json());
163
+ }
164
+ }
165
+ catch (err) {
166
+ if (this.debugMode)
167
+ console.error("Error sending log in statelog client:", err, {
168
+ host: this.host,
169
+ });
170
+ }
171
+ return failure("Error uploading files to statelog");
172
+ }
173
+ async remoteRun({ files, entrypoint, args, }) {
174
+ try {
175
+ const fullUrl = new URL(`/api/run`, this.host);
176
+ const url = fullUrl.toString();
177
+ const body = JSON.stringify({
178
+ files,
179
+ entrypoint,
180
+ args,
181
+ });
182
+ console.log({ entrypoint, args }, body);
183
+ const result = await fetch(url, {
184
+ method: "POST",
185
+ headers: {
186
+ "Content-Type": "application/json",
187
+ Authorization: `Bearer ${this.apiKey}`,
188
+ },
189
+ body,
190
+ }).catch((err) => {
191
+ if (this.debugMode)
192
+ console.error("Failed to run on statelog:", err);
193
+ });
194
+ if (result) {
195
+ if (!result.ok) {
196
+ if (this.debugMode) {
197
+ const responseBody = await result.text();
198
+ console.error("Failed to run on statelog:", {
199
+ result,
200
+ url,
201
+ body,
202
+ responseBody,
203
+ });
204
+ }
205
+ return failure("Failed to run on statelog");
206
+ }
207
+ return (await result.json());
208
+ }
209
+ }
210
+ catch (err) {
211
+ if (this.debugMode)
212
+ console.error("Error running on statelog client:", err, {
213
+ host: this.host,
214
+ });
215
+ }
216
+ return failure("Error running on statelog");
217
+ }
218
+ async hitServer({ userId, projectId, filename, nodeName, body, }) {
219
+ try {
220
+ const fullUrl = new URL(`/run/${userId}/${projectId}/${filename}/${nodeName}`, this.host);
221
+ const url = fullUrl.toString();
222
+ const result = await fetch(url, {
223
+ method: "POST",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ Authorization: `Bearer ${this.apiKey}`,
227
+ },
228
+ body: body,
229
+ }).catch((err) => {
230
+ if (this.debugMode)
231
+ console.error("Failed to run on statelog:", err);
232
+ });
233
+ if (result) {
234
+ if (!result.ok) {
235
+ if (this.debugMode) {
236
+ const responseBody = await result.text();
237
+ console.error("Failed to run on statelog:", {
238
+ result,
239
+ url,
240
+ body,
241
+ responseBody,
242
+ });
243
+ }
244
+ return failure("Failed to run on statelog");
245
+ }
246
+ return (await result.json());
247
+ }
248
+ }
249
+ catch (err) {
250
+ if (this.debugMode)
251
+ console.error("Error running on statelog client:", err, {
252
+ host: this.host,
253
+ });
254
+ }
255
+ return failure("Error running on statelog");
256
+ }
257
+ async post(body) {
258
+ if (!this.host) {
259
+ return;
260
+ }
261
+ const postBody = JSON.stringify({
262
+ trace_id: this.traceId,
263
+ project_id: this.projectId,
264
+ data: { ...body, timestamp: new Date().toISOString() },
265
+ });
266
+ if (this.host.toLowerCase() === "stdout") {
267
+ console.log(postBody);
268
+ return;
269
+ }
270
+ try {
271
+ const fullUrl = new URL("/api/logs", this.host);
272
+ const url = fullUrl.toString();
273
+ await fetch(url, {
274
+ method: "POST",
275
+ headers: {
276
+ "Content-Type": "application/json",
277
+ Authorization: `Bearer ${this.apiKey}`,
278
+ },
279
+ body: postBody,
280
+ }).catch((err) => {
281
+ if (this.debugMode)
282
+ console.error("Failed to send statelog:", err);
283
+ });
284
+ }
285
+ catch (err) {
286
+ if (this.debugMode)
287
+ console.error("Error sending log in statelog client:", err, {
288
+ host: this.host,
289
+ });
290
+ }
291
+ }
292
+ }
293
+ export function getStatelogClient(config) {
294
+ const statelogConfig = {
295
+ host: config.host,
296
+ traceId: config.traceId || nanoid(),
297
+ apiKey: process.env.STATELOG_API_KEY || "",
298
+ projectId: config.projectId,
299
+ debugMode: config.debugMode || false,
300
+ };
301
+ const client = new StatelogClient(statelogConfig);
302
+ return client;
303
+ }
package/dist/types.d.ts CHANGED
@@ -46,6 +46,12 @@ export type SmolConfig = {
46
46
  provider?: Provider;
47
47
  logLevel?: LogLevel;
48
48
  toolLoopDetection?: ToolLoopDetection;
49
+ statelog?: Partial<{
50
+ host: string;
51
+ projectId: string;
52
+ debugMode: boolean;
53
+ apiKey: string;
54
+ }>;
49
55
  };
50
56
  export type ToolLoopDetection = {
51
57
  enabled: boolean;
@@ -108,3 +114,10 @@ export type TextPart = {
108
114
  type: "text";
109
115
  text: string;
110
116
  };
117
+ export type JSONEdge = {
118
+ type: "regular";
119
+ to: string;
120
+ } | {
121
+ type: "conditional";
122
+ adjacentNodes: readonly string[];
123
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "scripts": {
@@ -44,6 +44,7 @@
44
44
  "@anthropic-ai/sdk": "^0.78.0",
45
45
  "@google/genai": "^1.34.0",
46
46
  "egonlog": "^0.0.2",
47
+ "nanoid": "^5.1.6",
47
48
  "ollama": "^0.6.3",
48
49
  "openai": "^6.15.0"
49
50
  }