hammer-ai 0.2.12 → 0.2.14

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.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # hammer-ai
2
+
3
+ Infrastructure for building tool-calling chat agents in TypeScript.
4
+
5
+ This package gives you:
6
+ - OpenAI-compatible LLM client with streaming support
7
+ - Agent loop runtime for web/chat interfaces
8
+ - Tool base class and tool registry
9
+ - Built-in handling for tool, bash, and background_bash run targets
10
+ - Memory and validation layers for longer multi-step workflows
11
+
12
+ The examples below are based on a real production-style integration from monoslides.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ bun add hammer-ai
18
+ ```
19
+
20
+ ## Mental model
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ UI[Chat UI] --> Runtime[WebToolLoopAgentRuntime]
25
+ Runtime --> LLM[LLMClient]
26
+ Runtime --> Registry[ToolRegistry]
27
+ Registry --> Local[Local Tool classes]
28
+ Registry --> Proxy[ProxyTool to server actions]
29
+ Registry --> Bash[Bash and BackgroundBash run commands]
30
+ Runtime --> Store[Runtime store]
31
+ Store --> UI
32
+ ```
33
+
34
+ ## 1. Configure providers (Qwen and DeepSeek)
35
+
36
+ Configure once at startup. You can keep multiple named presets and choose one for your runtime.
37
+
38
+ ```ts
39
+ import { configure } from "hammer-ai"
40
+
41
+ configure({
42
+ providers: {
43
+ "qwen-plus": {
44
+ apiKey: process.env.DASHSCOPE_API_KEY!,
45
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
46
+ model: "qwen-plus",
47
+ enableThinking: false,
48
+ },
49
+ deepseek: {
50
+ apiKey: process.env.DEEPSEEK_API_KEY!,
51
+ baseUrl: "https://api.deepseek.com",
52
+ model: "deepseek-v4-flash",
53
+ enableThinking: false,
54
+ },
55
+ "deepseek-reasoner": {
56
+ apiKey: process.env.DEEPSEEK_API_KEY!,
57
+ baseUrl: "https://api.deepseek.com",
58
+ model: "deepseek-reasoner",
59
+ enableThinking: true,
60
+ },
61
+ },
62
+ compactionProvider: "deepseek",
63
+ })
64
+ ```
65
+
66
+ Notes:
67
+ - For Qwen-style endpoints, enableThinking maps to enable_thinking.
68
+ - For DeepSeek endpoints, enableThinking maps to thinking.type (enabled or disabled).
69
+
70
+ ## 2. Create tools
71
+
72
+ Create local tools by extending Tool.
73
+
74
+ ```ts
75
+ import { Tool, type ToolResult, type ToolSchema } from "hammer-ai"
76
+
77
+ export class GetSlideFilePath extends Tool {
78
+ override getName(): string {
79
+ return "GetSlideFilePath"
80
+ }
81
+
82
+ override getDescription(): string {
83
+ return "Return the absolute path for the active slide SVG"
84
+ }
85
+
86
+ override getSchema(): ToolSchema {
87
+ return {
88
+ slideId: {
89
+ type: "string",
90
+ required: true,
91
+ positional: false,
92
+ description: "Slide identifier",
93
+ },
94
+ }
95
+ }
96
+
97
+ override async execute(params: Record<string, any>): Promise<ToolResult> {
98
+ const slideId = String(params.slideId ?? "").trim()
99
+ if (!slideId) return { success: false, error: "slideId is required" }
100
+
101
+ return {
102
+ success: true,
103
+ path: `/workspace/slide-${slideId}.svg`,
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## 3. Wire registry + proxy tools + bash
110
+
111
+ Use createToolRegistry for a mixed tool surface:
112
+ - local tool classes
113
+ - proxy tools that call server actions
114
+ - bash run command bindings
115
+
116
+ ```ts
117
+ import {
118
+ createToolRegistry,
119
+ ProxyTool,
120
+ createRunCommandRuntimeBindings,
121
+ BackgroundBashRunCommand,
122
+ } from "hammer-ai"
123
+ import { BashRunCommand } from "hammer-ai"
124
+
125
+ import { GetSlideFilePath } from "./tools/GetSlideFilePath"
126
+ import { CreateOrReplaceSlide } from "./tools/CreateOrReplaceSlide"
127
+ import { executeWebToolAction } from "./actions/web-tool-actions"
128
+
129
+ class AppBashRunCommand extends BashRunCommand {
130
+ protected override async executeCommand(command: string) {
131
+ // Route shell execution to your sandbox/session implementation.
132
+ return executeInSandbox(command)
133
+ }
134
+ }
135
+
136
+ const registry = createToolRegistry(
137
+ [
138
+ new ProxyTool(
139
+ {
140
+ name: "BraveWebSearch",
141
+ description: "Search the web",
142
+ parameters: {
143
+ query: {
144
+ type: "string",
145
+ description: "Search query",
146
+ required: true,
147
+ },
148
+ },
149
+ },
150
+ (parameters) => executeWebToolAction({ tool: "BraveWebSearch", input: parameters }),
151
+ ),
152
+ new GetSlideFilePath(),
153
+ new CreateOrReplaceSlide(),
154
+ ],
155
+ createRunCommandRuntimeBindings(
156
+ new AppBashRunCommand(),
157
+ new BackgroundBashRunCommand(),
158
+ ),
159
+ )
160
+
161
+ const executeTool = registry.createExecutor()
162
+ ```
163
+
164
+ ## 4. Build a chat runtime
165
+
166
+ WebToolLoopAgentRuntime is the easiest way to run a multi-step tool loop and stream content into UI state.
167
+
168
+ ```ts
169
+ import {
170
+ createRuntimeStore,
171
+ createInitialWebAgentState,
172
+ WebToolLoopAgentRuntime,
173
+ LLMClient,
174
+ getProviderConfig,
175
+ buildAgentIdentityLine,
176
+ buildWebRuntimeRules,
177
+ } from "hammer-ai"
178
+
179
+ const runtimeStore = createRuntimeStore(() => createInitialWebAgentState())
180
+
181
+ class SlidesRuntime extends WebToolLoopAgentRuntime {
182
+ constructor() {
183
+ super({
184
+ store: runtimeStore,
185
+ messageIdPrefix: "slides-msg",
186
+ llmClient: new LLMClient({
187
+ ...getProviderConfig("deepseek"),
188
+ enableThinking: false,
189
+ }),
190
+ getToolDefinitions: () => registry.getToolDefinitions(),
191
+ executeTool,
192
+ memoryPreset: "monoslides",
193
+ systemIdentity: buildAgentIdentityLine({
194
+ agentName: "MonoSlides",
195
+ roleDescription: "Slide design assistant",
196
+ }),
197
+ extraRules: buildWebRuntimeRules({
198
+ includeVerifiedCompletionRule: true,
199
+ }),
200
+ temperature: 0.2,
201
+ maxTokens: 8192,
202
+ allowedRunTargets: ["tool", "bash"],
203
+ })
204
+ }
205
+
206
+ async run(userTask: string): Promise<void> {
207
+ await this.executeDefaultWebRun(userTask, {
208
+ shouldSurfaceAssistantContent: (_reasoning, rawContent) => Boolean(rawContent),
209
+ })
210
+ }
211
+
212
+ abort(): void {
213
+ this.defaultWebAbort()
214
+ }
215
+
216
+ reset(): void {
217
+ this.defaultWebReset()
218
+ }
219
+ }
220
+
221
+ export const runtime = new SlidesRuntime()
222
+ ```
223
+
224
+ ## 5. Expose controller actions
225
+
226
+ A small controller wrapper keeps runtime actions and store in one object.
227
+
228
+ ```ts
229
+ import { defineRuntimeController } from "hammer-ai"
230
+
231
+ export const chatController = defineRuntimeController({
232
+ store: runtimeStore,
233
+ actions: {
234
+ run: (prompt: string) => runtime.run(prompt),
235
+ abort: () => runtime.abort(),
236
+ reset: () => runtime.reset(),
237
+ },
238
+ refs: {} as const,
239
+ })
240
+ ```
241
+
242
+ ## 6. Render in UI
243
+
244
+ You can subscribe directly with useSyncExternalStore.
245
+
246
+ ```tsx
247
+ import { useSyncExternalStore, useState } from "react"
248
+
249
+ function useRuntimeState() {
250
+ return useSyncExternalStore(
251
+ chatController.store.subscribe,
252
+ chatController.store.getSnapshot,
253
+ chatController.store.getServerSnapshot,
254
+ )
255
+ }
256
+
257
+ export function ChatPanel() {
258
+ const state = useRuntimeState()
259
+ const [input, setInput] = useState("")
260
+ const isRunning = state.phase === "thinking" || state.phase === "tool-calling"
261
+
262
+ return (
263
+ <div>
264
+ <div>
265
+ {state.messages.map((message) => (
266
+ <div key={message.id}>{message.content}</div>
267
+ ))}
268
+ {state.streamingContent ? <div>{state.streamingContent}</div> : null}
269
+ </div>
270
+
271
+ <form
272
+ onSubmit={(event) => {
273
+ event.preventDefault()
274
+ const next = input.trim()
275
+ if (!next || isRunning) return
276
+ chatController.actions.run(next)
277
+ setInput("")
278
+ }}
279
+ >
280
+ <textarea value={input} onChange={(event) => setInput(event.target.value)} />
281
+ {isRunning ? (
282
+ <button type="button" onClick={() => chatController.actions.abort()}>Stop</button>
283
+ ) : (
284
+ <button type="submit">Send</button>
285
+ )}
286
+ </form>
287
+ </div>
288
+ )
289
+ }
290
+ ```
291
+
292
+ ## 7. Recommended production patterns
293
+
294
+ - Keep local, deterministic operations as Tool subclasses.
295
+ - Wrap privileged/server-only capabilities (web calls, secrets, DB) with ProxyTool.
296
+ - Give the model only the run targets you want via allowedRunTargets.
297
+ - Keep strict completion rules (for example: todo list must be completed before exit).
298
+ - Use executeDefaultWebRun for serialization, abort handling, and consistent state transitions.
299
+ - Prefer enableThinking: false for latency-sensitive UX, and enable it only for tasks that need deeper reasoning.
300
+
301
+ ## 8. Minimal server action bridge example
302
+
303
+ ```ts
304
+ import { createWebSearchToolActions } from "hammer-ai"
305
+
306
+ const actions = createWebSearchToolActions({
307
+ executeWebSearch: (tool, input) => {
308
+ if (tool === "BochaWebSearch") return bocha.execute(input)
309
+ return brave.execute(input)
310
+ },
311
+ })
312
+
313
+ export async function executeWebToolAction(input: {
314
+ tool: "BraveWebSearch" | "BochaWebSearch"
315
+ input: Record<string, unknown>
316
+ }) {
317
+ return actions.executeWebTool(input)
318
+ }
319
+ ```
320
+
321
+ ## API Surface
322
+
323
+ Main exports used in this guide:
324
+ - configure, getProviderConfig
325
+ - LLMClient
326
+ - Tool, ProxyTool, SubAgentTool
327
+ - createToolRegistry, createRunCommandRuntimeBindings
328
+ - BashRunCommand, BackgroundBashRunCommand
329
+ - WebToolLoopAgentRuntime
330
+ - createRuntimeStore, defineRuntimeController
331
+ - createInitialWebAgentState
332
+ - createWebSearchToolActions
package/dist/index.d.ts CHANGED
@@ -32,14 +32,14 @@ interface LLMProviderConfig {
32
32
  * Explicitly enable or disable the provider's thinking/reasoning mode.
33
33
  *
34
34
  * - `false` — disables thinking (e.g. DashScope `enable_thinking: false` for
35
- * Qwen3 models, which have thinking on by default). Prevents the silent
36
- * multi-minute server-side CoT delay before the first token streams out.
35
+ * Qwen3 models, or DeepSeek `thinking: { type: "disabled" }`).
37
36
  * - `true` — explicitly enables thinking with the provider's default budget.
38
37
  * - `undefined` — no thinking-related field is sent; the provider uses its
39
38
  * own model default.
40
39
  *
41
- * Currently maps to `enable_thinking` in the request body, which is the
42
- * DashScope OpenAI-compatible API parameter for Qwen3 models.
40
+ * Currently maps to provider-specific request fields:
41
+ * - Qwen/DashScope: `enable_thinking`
42
+ * - DeepSeek: `thinking: { type: "enabled" | "disabled" }`
43
43
  */
44
44
  enableThinking?: boolean;
45
45
  }
@@ -176,7 +176,7 @@ interface LLMResponse {
176
176
  outcome: LoopOutcome;
177
177
  finishReason?: string;
178
178
  }
179
- type ProviderName = "qwen-max" | "qwen-plus" | "openrouter-claude" | "openrouter-gemini" | "minimax" | "minimax-her" | "chatglm" | "kimi" | "doubao";
179
+ type ProviderName = "qwen-max" | "qwen-plus" | "deepseek" | "deepseek-chat" | "deepseek-reasoner" | "openrouter-claude" | "openrouter-gemini" | "minimax" | "minimax-her" | "chatglm" | "kimi" | "doubao";
180
180
 
181
181
  /**
182
182
  * configure.ts — global configuration store for hammer-agent.
@@ -195,6 +195,8 @@ interface HammerAgentProviderPreset {
195
195
  model: string;
196
196
  /** Extra headers merged into every request (e.g. HTTP-Referer for OpenRouter). */
197
197
  extraHeaders?: Record<string, string>;
198
+ /** Provider-specific thinking/reasoning toggle (Qwen/DeepSeek). */
199
+ enableThinking?: boolean;
198
200
  }
199
201
  /**
200
202
  * Options accepted by `configure()`.
@@ -875,8 +877,6 @@ interface MemoryLayerConfig {
875
877
  protectedContextTokens: number;
876
878
  /** Maximum tokens for the rendered compressed state. */
877
879
  stateBudgetTokens: number;
878
- /** Hard cap on raw history entries (safety net). */
879
- maxRawHistory: number;
880
880
  /** Minimum turns between compaction attempts. */
881
881
  compactionDebounceTurns: number;
882
882
  /** Baseline token overhead for system prompt (conservative estimate). */
@@ -899,7 +899,7 @@ interface MemoryLayerConfig {
899
899
  * - State rendering (converting TState to human-readable text)
900
900
  *
901
901
  * The base class provides:
902
- * - Append-only raw history with hard cap enforcement
902
+ * - Append-only raw history
903
903
  * - Token-budgeted sliding window for recent messages
904
904
  * - buildMessages assembly (system → state → recent)
905
905
  * - Compaction triggering (debounce, threshold check, prune)
@@ -1152,8 +1152,6 @@ interface AgentMemoryLayerConfig {
1152
1152
  protectedContextTokens: number;
1153
1153
  /** Token budget for the rendered compressed state block. */
1154
1154
  stateBudgetTokens: number;
1155
- /** Hard cap on raw history entries. */
1156
- maxRawHistory: number;
1157
1155
  /** Minimum turns between compaction attempts. */
1158
1156
  compactionDebounceTurns: number;
1159
1157
  /** Token estimate for the system prompt. */
package/dist/index.js CHANGED
@@ -213,7 +213,13 @@ var LLMClient = class {
213
213
  delete payload.temperature;
214
214
  }
215
215
  if (this.config.enableThinking !== void 0) {
216
- payload.enable_thinking = this.config.enableThinking;
216
+ if (isDeepSeekRequest(this.config)) {
217
+ payload.thinking = {
218
+ type: this.config.enableThinking ? "enabled" : "disabled"
219
+ };
220
+ } else {
221
+ payload.enable_thinking = this.config.enableThinking;
222
+ }
217
223
  }
218
224
  const headers = {
219
225
  "Content-Type": "application/json",
@@ -465,6 +471,9 @@ var LLMClient = class {
465
471
  };
466
472
  }
467
473
  };
474
+ function isDeepSeekRequest(config) {
475
+ return /deepseek/i.test(config.model) || /deepseek\.com/i.test(config.baseUrl);
476
+ }
468
477
  var ApiError = class extends Error {
469
478
  constructor(status, body) {
470
479
  super(`API error ${status}: ${body}`);
@@ -4154,14 +4163,6 @@ var BaseMemoryLayer = class {
4154
4163
  const timestamp = Date.now();
4155
4164
  const msg = this.createMessage(id, role, content, this.currentTurn, timestamp);
4156
4165
  this.rawHistory.push(msg);
4157
- if (this.rawHistory.length > this.config.maxRawHistory) {
4158
- const excess = this.rawHistory.length - this.config.maxRawHistory;
4159
- const lastPrunedTurn = this.rawHistory[excess - 1].turn;
4160
- this.rawHistory.splice(0, excess);
4161
- if (this.compactionCursor.lastCompactedTurn < lastPrunedTurn) {
4162
- this.compactionCursor.lastCompactedTurn = lastPrunedTurn;
4163
- }
4164
- }
4165
4166
  this.onMessageAppended(msg);
4166
4167
  return id;
4167
4168
  }
@@ -5227,7 +5228,6 @@ var AgentMemoryLayer = class extends BaseMemoryLayer {
5227
5228
  compactionTokenThreshold: config.compactionTokenThreshold,
5228
5229
  protectedContextTokens: config.protectedContextTokens,
5229
5230
  stateBudgetTokens: config.stateBudgetTokens,
5230
- maxRawHistory: config.maxRawHistory,
5231
5231
  compactionDebounceTurns: config.compactionDebounceTurns,
5232
5232
  systemPromptOverhead: config.systemPromptOverhead,
5233
5233
  tokenEstimator: config.tokenEstimator,
@@ -5734,7 +5734,6 @@ var SHARED_WORKSPACE_AGENT_MEMORY_PRESET = {
5734
5734
  // 60_000
5735
5735
  stateBudgetTokens: Math.floor(DEFAULT_MAX_CONTEXT_TOKENS * 0.05),
5736
5736
  // 10_000
5737
- maxRawHistory: 2e3,
5738
5737
  compactionDebounceTurns: 3,
5739
5738
  systemPromptOverhead: 4e3,
5740
5739
  toolMemoryExtractor: DEFAULT_TOOL_MEMORY_EXTRACTOR
@@ -5744,7 +5743,6 @@ function createAgentMemoryLayer(_preset, overrides) {
5744
5743
  compactionTokenThreshold: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.compactionTokenThreshold,
5745
5744
  protectedContextTokens: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.protectedContextTokens,
5746
5745
  stateBudgetTokens: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.stateBudgetTokens,
5747
- maxRawHistory: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.maxRawHistory,
5748
5746
  compactionDebounceTurns: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.compactionDebounceTurns,
5749
5747
  systemPromptOverhead: SHARED_WORKSPACE_AGENT_MEMORY_PRESET.systemPromptOverhead,
5750
5748
  tokenEstimator: overrides?.tokenEstimator ?? new CharTokenEstimator(),