smoltalk 0.0.34 → 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,5 +1,5 @@
1
1
  import { BaseMessage, MessageClass } from "./BaseMessage.js";
2
- import { TextPart } from "../../types.js";
2
+ import { TextPart, ThinkingBlock } from "../../types.js";
3
3
  import { ChatCompletionMessageParam } from "openai/resources";
4
4
  import { Content } from "@google/genai";
5
5
  import { ToolCall, ToolCallJSON } from "../ToolCall.js";
@@ -12,6 +12,7 @@ export type AssistantMessageJSON = {
12
12
  audio: any | null | undefined;
13
13
  refusal: string | null | undefined;
14
14
  toolCalls: ToolCallJSON[] | undefined;
15
+ thinkingBlocks: ThinkingBlock[] | undefined;
15
16
  };
16
17
  export declare class AssistantMessage extends BaseMessage implements MessageClass {
17
18
  _role: "assistant";
@@ -20,12 +21,14 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
20
21
  _audio?: any | null;
21
22
  _refusal?: string | null;
22
23
  _toolCalls?: ToolCall[];
24
+ _thinkingBlocks?: ThinkingBlock[];
23
25
  _rawData?: any;
24
26
  constructor(content: string | Array<TextPart> | null, options?: {
25
27
  name?: string;
26
28
  audio?: any | null;
27
29
  refusal?: string | null;
28
30
  toolCalls?: ToolCall[];
31
+ thinkingBlocks?: ThinkingBlock[];
29
32
  rawData?: any;
30
33
  });
31
34
  get content(): string;
@@ -36,6 +39,7 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
36
39
  get refusal(): string | null | undefined;
37
40
  get toolCalls(): ToolCall[] | undefined;
38
41
  get rawData(): any;
42
+ get thinkingBlocks(): ThinkingBlock[] | undefined;
39
43
  toJSON(): AssistantMessageJSON;
40
44
  static fromJSON(json: any): AssistantMessage;
41
45
  toOpenAIMessage(): ChatCompletionMessageParam;
@@ -45,6 +49,10 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
45
49
  toAnthropicMessage(): {
46
50
  role: "assistant";
47
51
  content: string | Array<{
52
+ type: "thinking";
53
+ thinking: string;
54
+ signature: string;
55
+ } | {
48
56
  type: "text";
49
57
  text: string;
50
58
  } | {
@@ -7,6 +7,7 @@ export class AssistantMessage extends BaseMessage {
7
7
  _audio;
8
8
  _refusal;
9
9
  _toolCalls;
10
+ _thinkingBlocks;
10
11
  _rawData;
11
12
  constructor(content, options = {}) {
12
13
  super();
@@ -15,6 +16,7 @@ export class AssistantMessage extends BaseMessage {
15
16
  this._audio = options.audio;
16
17
  this._refusal = options.refusal;
17
18
  this._toolCalls = options.toolCalls;
19
+ this._thinkingBlocks = options.thinkingBlocks;
18
20
  this._rawData = options.rawData;
19
21
  }
20
22
  get content() {
@@ -46,6 +48,9 @@ export class AssistantMessage extends BaseMessage {
46
48
  get rawData() {
47
49
  return this._rawData;
48
50
  }
51
+ get thinkingBlocks() {
52
+ return this._thinkingBlocks;
53
+ }
49
54
  toJSON() {
50
55
  return {
51
56
  role: this.role,
@@ -54,6 +59,7 @@ export class AssistantMessage extends BaseMessage {
54
59
  audio: this.audio,
55
60
  refusal: this.refusal,
56
61
  toolCalls: this.toolCalls?.map((tc) => tc.toJSON()),
62
+ thinkingBlocks: this._thinkingBlocks,
57
63
  };
58
64
  }
59
65
  static fromJSON(json) {
@@ -64,6 +70,7 @@ export class AssistantMessage extends BaseMessage {
64
70
  toolCalls: json.toolCalls
65
71
  ? json.toolCalls.map((tcJson) => ToolCall.fromJSON(tcJson))
66
72
  : undefined,
73
+ thinkingBlocks: json.thinkingBlocks,
67
74
  rawData: json.rawData,
68
75
  });
69
76
  }
@@ -95,6 +102,12 @@ export class AssistantMessage extends BaseMessage {
95
102
  }
96
103
  toGoogleMessage() {
97
104
  const parts = [];
105
+ // Prepend thought parts with their signatures so Gemini can resume reasoning
106
+ if (this._thinkingBlocks) {
107
+ for (const block of this._thinkingBlocks) {
108
+ parts.push({ thought: true, text: block.text, thoughtSignature: block.signature });
109
+ }
110
+ }
98
111
  if (this.content) {
99
112
  parts.push({ text: this.content });
100
113
  }
@@ -119,10 +132,18 @@ export class AssistantMessage extends BaseMessage {
119
132
  ? this._content.length > 0
120
133
  : this._content.length > 0);
121
134
  const hasToolCalls = this._toolCalls && this._toolCalls.length > 0;
122
- if (!hasToolCalls) {
135
+ const hasThinking = this._thinkingBlocks && this._thinkingBlocks.length > 0;
136
+ // If only text and no thinking/tool calls, use string shorthand
137
+ if (!hasToolCalls && !hasThinking) {
123
138
  return { role: "assistant", content: this.content };
124
139
  }
125
140
  const blocks = [];
141
+ // Thinking blocks must come first (Anthropic requires this ordering)
142
+ if (hasThinking) {
143
+ for (const block of this._thinkingBlocks) {
144
+ blocks.push({ type: "thinking", thinking: block.text, signature: block.signature });
145
+ }
146
+ }
126
147
  if (hasText) {
127
148
  const text = typeof this._content === "string"
128
149
  ? this._content
@@ -131,7 +152,7 @@ export class AssistantMessage extends BaseMessage {
131
152
  blocks.push({ type: "text", text });
132
153
  }
133
154
  }
134
- for (const tc of this._toolCalls) {
155
+ for (const tc of this._toolCalls ?? []) {
135
156
  blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
136
157
  }
137
158
  return { role: "assistant", content: blocks };
@@ -25,6 +25,10 @@ export declare function assistantMessage(content: string | Array<TextPart> | nul
25
25
  audio?: any | null;
26
26
  refusal?: string | null;
27
27
  toolCalls?: Array<any>;
28
+ thinkingBlocks?: Array<{
29
+ text: string;
30
+ signature: string;
31
+ }>;
28
32
  rawData?: any;
29
33
  }): AssistantMessage;
30
34
  export declare function developerMessage(content: string | Array<TextPart>, options?: {
@@ -64,16 +64,20 @@ export class SmolAnthropic extends BaseClient {
64
64
  description: tool.description,
65
65
  }))
66
66
  : undefined;
67
- return { system, messages: anthropicMessages, tools };
67
+ const thinking = config.thinking?.enabled
68
+ ? { type: "enabled", budget_tokens: config.thinking.budgetTokens ?? 5000 }
69
+ : undefined;
70
+ return { system, messages: anthropicMessages, tools, thinking };
68
71
  }
69
72
  async _textSync(config) {
70
- const { system, messages, tools } = this.buildRequest(config);
73
+ const { system, messages, tools, thinking } = this.buildRequest(config);
71
74
  this.logger.debug("Sending request to Anthropic:", {
72
75
  model: this.model,
73
76
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
74
77
  messages,
75
78
  system,
76
79
  tools,
80
+ thinking,
77
81
  });
78
82
  const response = await this.client.messages.create({
79
83
  model: this.model,
@@ -81,6 +85,7 @@ export class SmolAnthropic extends BaseClient {
81
85
  messages,
82
86
  ...(system && { system }),
83
87
  ...(tools && { tools }),
88
+ ...(thinking && { thinking }),
84
89
  ...(config.temperature !== undefined && {
85
90
  temperature: config.temperature,
86
91
  }),
@@ -90,6 +95,7 @@ export class SmolAnthropic extends BaseClient {
90
95
  this.logger.debug("Response from Anthropic:", response);
91
96
  let output = null;
92
97
  const toolCalls = [];
98
+ const thinkingBlocks = [];
93
99
  for (const block of response.content) {
94
100
  if (block.type === "text") {
95
101
  output = (output ?? "") + block.text;
@@ -97,24 +103,30 @@ export class SmolAnthropic extends BaseClient {
97
103
  else if (block.type === "tool_use") {
98
104
  toolCalls.push(new ToolCall(block.id, block.name, block.input));
99
105
  }
106
+ else if (block.type === "thinking") {
107
+ const b = block;
108
+ thinkingBlocks.push({ text: b.thinking, signature: b.signature });
109
+ }
100
110
  }
101
111
  const { usage, cost } = this.calculateUsageAndCost(response.usage);
102
112
  return success({
103
113
  output,
104
114
  toolCalls,
115
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
105
116
  usage,
106
117
  cost,
107
118
  model: this.model,
108
119
  });
109
120
  }
110
121
  async *_textStream(config) {
111
- const { system, messages, tools } = this.buildRequest(config);
122
+ const { system, messages, tools, thinking } = this.buildRequest(config);
112
123
  this.logger.debug("Sending streaming request to Anthropic:", {
113
124
  model: this.model,
114
125
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
115
126
  messages,
116
127
  system,
117
128
  tools,
129
+ thinking,
118
130
  });
119
131
  const stream = await this.client.messages.create({
120
132
  model: this.model,
@@ -122,6 +134,7 @@ export class SmolAnthropic extends BaseClient {
122
134
  messages,
123
135
  ...(system && { system }),
124
136
  ...(tools && { tools }),
137
+ ...(thinking && { thinking }),
125
138
  ...(config.temperature !== undefined && {
126
139
  temperature: config.temperature,
127
140
  }),
@@ -131,19 +144,25 @@ export class SmolAnthropic extends BaseClient {
131
144
  let content = "";
132
145
  // Track tool blocks by index: index -> { id, name, arguments (partial JSON) }
133
146
  const toolBlocks = new Map();
147
+ // Track thinking blocks by index: index -> { text, signature }
148
+ const thinkingBlockMap = new Map();
134
149
  let inputTokens = 0;
135
150
  let outputTokens = 0;
136
151
  for await (const event of stream) {
137
152
  if (event.type === "message_start") {
138
153
  inputTokens = event.message.usage.input_tokens;
139
154
  }
140
- else if (event.type === "content_block_start" &&
141
- event.content_block.type === "tool_use") {
142
- toolBlocks.set(event.index, {
143
- id: event.content_block.id,
144
- name: event.content_block.name,
145
- arguments: "",
146
- });
155
+ else if (event.type === "content_block_start") {
156
+ if (event.content_block.type === "tool_use") {
157
+ toolBlocks.set(event.index, {
158
+ id: event.content_block.id,
159
+ name: event.content_block.name,
160
+ arguments: "",
161
+ });
162
+ }
163
+ else if (event.content_block.type === "thinking") {
164
+ thinkingBlockMap.set(event.index, { text: "", signature: "" });
165
+ }
147
166
  }
148
167
  else if (event.type === "content_block_delta") {
149
168
  if (event.delta.type === "text_delta") {
@@ -156,6 +175,25 @@ export class SmolAnthropic extends BaseClient {
156
175
  block.arguments += event.delta.partial_json;
157
176
  }
158
177
  }
178
+ else if (event.delta.type === "thinking_delta") {
179
+ const block = thinkingBlockMap.get(event.index);
180
+ if (block) {
181
+ block.text += event.delta.thinking;
182
+ }
183
+ }
184
+ else if (event.delta.type === "signature_delta") {
185
+ const block = thinkingBlockMap.get(event.index);
186
+ if (block) {
187
+ block.signature = event.delta.signature;
188
+ }
189
+ }
190
+ }
191
+ else if (event.type === "content_block_stop") {
192
+ // Emit thinking chunk once the block is fully assembled
193
+ const thinkingBlock = thinkingBlockMap.get(event.index);
194
+ if (thinkingBlock) {
195
+ yield { type: "thinking", text: thinkingBlock.text, signature: thinkingBlock.signature };
196
+ }
159
197
  }
160
198
  else if (event.type === "message_delta") {
161
199
  outputTokens = event.usage.output_tokens;
@@ -168,6 +206,7 @@ export class SmolAnthropic extends BaseClient {
168
206
  toolCalls.push(toolCall);
169
207
  yield { type: "tool_call", toolCall };
170
208
  }
209
+ const thinkingBlocks = Array.from(thinkingBlockMap.values());
171
210
  const usage = {
172
211
  inputTokens,
173
212
  outputTokens,
@@ -179,6 +218,7 @@ export class SmolAnthropic extends BaseClient {
179
218
  result: {
180
219
  output: content || null,
181
220
  toolCalls,
221
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
182
222
  usage,
183
223
  cost,
184
224
  model: this.model,
@@ -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),
@@ -91,6 +91,7 @@ export class SmolGoogle extends BaseClient {
91
91
  this.logger.debug("Response from Google Gemini:", JSON.stringify(result, null, 2));
92
92
  const output = result.text || null;
93
93
  const toolCalls = [];
94
+ const thinkingBlocks = [];
94
95
  result.candidates?.forEach((candidate) => {
95
96
  if (candidate.content && candidate.content.parts) {
96
97
  candidate.content.parts.forEach((part) => {
@@ -98,6 +99,13 @@ export class SmolGoogle extends BaseClient {
98
99
  const functionCall = part.functionCall;
99
100
  toolCalls.push(new ToolCall("", functionCall.name, functionCall.args));
100
101
  }
102
+ // Capture thought parts (thought: true indicates a thinking part)
103
+ if (part.thoughtSignature) {
104
+ thinkingBlocks.push({
105
+ text: part.text || "",
106
+ signature: part.thoughtSignature,
107
+ });
108
+ }
101
109
  });
102
110
  }
103
111
  });
@@ -107,6 +115,7 @@ export class SmolGoogle extends BaseClient {
107
115
  return success({
108
116
  output,
109
117
  toolCalls,
118
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
110
119
  usage,
111
120
  cost,
112
121
  model: request.model,
@@ -118,6 +127,7 @@ export class SmolGoogle extends BaseClient {
118
127
  const stream = await this.client.models.generateContentStream(request);
119
128
  let content = "";
120
129
  const toolCallsMap = new Map();
130
+ const thinkingBlocks = [];
121
131
  let usage;
122
132
  let cost;
123
133
  for await (const chunk of stream) {
@@ -127,22 +137,28 @@ export class SmolGoogle extends BaseClient {
127
137
  usage = usageAndCost.usage;
128
138
  cost = usageAndCost.cost;
129
139
  }
130
- // Handle text content
131
- if (chunk.text) {
132
- content += chunk.text;
133
- yield { type: "text", text: chunk.text };
134
- }
135
- // Handle function calls
136
- if (chunk.functionCalls) {
137
- for (const functionCall of chunk.functionCalls) {
138
- const id = functionCall.id || functionCall.name || "";
139
- const name = functionCall.name || "";
140
- if (!toolCallsMap.has(id)) {
141
- toolCallsMap.set(id, {
142
- id: id,
143
- name: name,
144
- arguments: functionCall.args,
145
- });
140
+ // Iterate raw parts to capture thought signatures and regular content
141
+ for (const candidate of chunk.candidates || []) {
142
+ for (const part of candidate?.content?.parts || []) {
143
+ const p = part;
144
+ if (p.thoughtSignature) {
145
+ const block = {
146
+ text: p.text || "",
147
+ signature: p.thoughtSignature,
148
+ };
149
+ thinkingBlocks.push(block);
150
+ yield { type: "thinking", text: block.text, signature: block.signature };
151
+ }
152
+ else if (p.text) {
153
+ content += p.text;
154
+ yield { type: "text", text: p.text };
155
+ }
156
+ else if (p.functionCall) {
157
+ const id = p.functionCall.id || p.functionCall.name || "";
158
+ const name = p.functionCall.name || "";
159
+ if (!toolCallsMap.has(id)) {
160
+ toolCallsMap.set(id, { id, name, arguments: p.functionCall.args });
161
+ }
146
162
  }
147
163
  }
148
164
  }
@@ -160,6 +176,7 @@ export class SmolGoogle extends BaseClient {
160
176
  result: {
161
177
  output: content || null,
162
178
  toolCalls,
179
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
163
180
  usage,
164
181
  cost,
165
182
  model: request.model,
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
@@ -5,6 +5,10 @@ import { Message } from "./classes/message/index.js";
5
5
  import { ToolCall } from "./classes/ToolCall.js";
6
6
  import { ModelConfig, ModelName, Provider } from "./models.js";
7
7
  import { Result } from "./types/result.js";
8
+ export type ThinkingBlock = {
9
+ text: string;
10
+ signature: string;
11
+ };
8
12
  export type PromptConfig = {
9
13
  messages: Message[];
10
14
  tools?: {
@@ -19,6 +23,10 @@ export type PromptConfig = {
19
23
  parallelToolCalls?: boolean;
20
24
  responseFormat?: ZodType;
21
25
  stream?: boolean;
26
+ thinking?: {
27
+ enabled: boolean;
28
+ budgetTokens?: number;
29
+ };
22
30
  responseFormatOptions?: Partial<{
23
31
  name: string;
24
32
  strict: boolean;
@@ -38,6 +46,12 @@ export type SmolConfig = {
38
46
  provider?: Provider;
39
47
  logLevel?: LogLevel;
40
48
  toolLoopDetection?: ToolLoopDetection;
49
+ statelog?: Partial<{
50
+ host: string;
51
+ projectId: string;
52
+ debugMode: boolean;
53
+ apiKey: string;
54
+ }>;
41
55
  };
42
56
  export type ToolLoopDetection = {
43
57
  enabled: boolean;
@@ -65,6 +79,7 @@ export type CostEstimate = {
65
79
  export type PromptResult = {
66
80
  output: string | null;
67
81
  toolCalls: ToolCall[];
82
+ thinkingBlocks?: ThinkingBlock[];
68
83
  usage?: TokenUsage;
69
84
  cost?: CostEstimate;
70
85
  model?: ModelName | ModelConfig;
@@ -72,6 +87,10 @@ export type PromptResult = {
72
87
  export type StreamChunk = {
73
88
  type: "text";
74
89
  text: string;
90
+ } | {
91
+ type: "thinking";
92
+ text: string;
93
+ signature?: string;
75
94
  } | {
76
95
  type: "tool_call";
77
96
  toolCall: ToolCall;
@@ -95,3 +114,10 @@ export type TextPart = {
95
114
  type: "text";
96
115
  text: string;
97
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.34",
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,7 +44,8 @@
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
  }
50
- }
51
+ }