@soederpop/luca 0.0.31 → 0.0.34

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.
Files changed (86) hide show
  1. package/README.md +241 -36
  2. package/bun.lock +24 -5
  3. package/commands/build-python-bridge.ts +43 -0
  4. package/docs/apis/clients/rest.md +7 -7
  5. package/docs/apis/clients/websocket.md +23 -10
  6. package/docs/apis/features/agi/assistant.md +155 -8
  7. package/docs/apis/features/agi/assistants-manager.md +90 -22
  8. package/docs/apis/features/agi/auto-assistant.md +377 -0
  9. package/docs/apis/features/agi/browser-use.md +802 -0
  10. package/docs/apis/features/agi/claude-code.md +6 -1
  11. package/docs/apis/features/agi/conversation-history.md +7 -6
  12. package/docs/apis/features/agi/conversation.md +111 -38
  13. package/docs/apis/features/agi/docs-reader.md +35 -57
  14. package/docs/apis/features/agi/file-tools.md +163 -0
  15. package/docs/apis/features/agi/openapi.md +2 -2
  16. package/docs/apis/features/agi/skills-library.md +227 -0
  17. package/docs/apis/features/node/content-db.md +125 -4
  18. package/docs/apis/features/node/disk-cache.md +11 -11
  19. package/docs/apis/features/node/downloader.md +1 -1
  20. package/docs/apis/features/node/file-manager.md +15 -15
  21. package/docs/apis/features/node/fs.md +78 -21
  22. package/docs/apis/features/node/git.md +50 -10
  23. package/docs/apis/features/node/google-calendar.md +3 -0
  24. package/docs/apis/features/node/google-docs.md +10 -1
  25. package/docs/apis/features/node/google-drive.md +3 -0
  26. package/docs/apis/features/node/google-mail.md +214 -0
  27. package/docs/apis/features/node/google-sheets.md +3 -0
  28. package/docs/apis/features/node/ink.md +10 -10
  29. package/docs/apis/features/node/ipc-socket.md +83 -93
  30. package/docs/apis/features/node/networking.md +5 -5
  31. package/docs/apis/features/node/os.md +7 -7
  32. package/docs/apis/features/node/package-finder.md +14 -14
  33. package/docs/apis/features/node/proc.md +2 -1
  34. package/docs/apis/features/node/process-manager.md +70 -3
  35. package/docs/apis/features/node/python.md +265 -9
  36. package/docs/apis/features/node/redis.md +380 -0
  37. package/docs/apis/features/node/ui.md +13 -13
  38. package/docs/apis/servers/express.md +35 -7
  39. package/docs/apis/servers/mcp.md +3 -3
  40. package/docs/apis/servers/websocket.md +51 -8
  41. package/docs/bootstrap/CLAUDE.md +1 -1
  42. package/docs/bootstrap/SKILL.md +93 -7
  43. package/docs/examples/feature-as-tool-provider.md +143 -0
  44. package/docs/examples/python.md +42 -1
  45. package/docs/introspection.md +15 -5
  46. package/docs/tutorials/00-bootstrap.md +3 -3
  47. package/docs/tutorials/02-container.md +2 -2
  48. package/docs/tutorials/10-creating-features.md +5 -0
  49. package/docs/tutorials/13-introspection.md +12 -2
  50. package/docs/tutorials/19-python-sessions.md +401 -0
  51. package/package.json +8 -4
  52. package/src/agi/container.server.ts +8 -0
  53. package/src/agi/features/assistant.ts +19 -0
  54. package/src/agi/features/autonomous-assistant.ts +435 -0
  55. package/src/agi/features/conversation.ts +58 -6
  56. package/src/agi/features/file-tools.ts +286 -0
  57. package/src/agi/features/luca-coder.ts +643 -0
  58. package/src/bootstrap/generated.ts +705 -17
  59. package/src/cli/build-info.ts +2 -2
  60. package/src/cli/cli.ts +22 -13
  61. package/src/commands/bootstrap.ts +49 -6
  62. package/src/commands/code.ts +369 -0
  63. package/src/commands/describe.ts +7 -2
  64. package/src/commands/index.ts +1 -0
  65. package/src/commands/sandbox-mcp.ts +7 -7
  66. package/src/commands/save-api-docs.ts +1 -1
  67. package/src/container-describer.ts +4 -4
  68. package/src/container.ts +10 -19
  69. package/src/helper.ts +24 -33
  70. package/src/introspection/generated.agi.ts +2499 -63
  71. package/src/introspection/generated.node.ts +1625 -688
  72. package/src/introspection/generated.web.ts +15 -57
  73. package/src/node/container.ts +5 -0
  74. package/src/node/features/figlet-fonts.ts +597 -0
  75. package/src/node/features/fs.ts +3 -9
  76. package/src/node/features/helpers.ts +20 -0
  77. package/src/node/features/python.ts +429 -16
  78. package/src/node/features/redis.ts +446 -0
  79. package/src/node/features/ui.ts +4 -11
  80. package/src/python/bridge.py +220 -0
  81. package/src/python/generated.ts +227 -0
  82. package/src/scaffolds/generated.ts +1 -1
  83. package/test/python-session.test.ts +105 -0
  84. package/assistants/lucaExpert/CORE.md +0 -37
  85. package/assistants/lucaExpert/hooks.ts +0 -9
  86. package/assistants/lucaExpert/tools.ts +0 -177
@@ -0,0 +1,435 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { Feature } from '@soederpop/luca/feature'
4
+ import type { AGIContainer } from '../container.server.js'
5
+ import type { Assistant } from './assistant.js'
6
+ import type { ToolCallCtx } from '../lib/interceptor-chain.js'
7
+
8
+ declare module '@soederpop/luca/feature' {
9
+ interface AvailableFeatures {
10
+ autoAssistant: typeof AutonomousAssistant
11
+ }
12
+ }
13
+
14
+ /** Permission level for a tool. 'allow' runs immediately, 'ask' blocks for user approval, 'deny' rejects. */
15
+ export type PermissionLevel = 'allow' | 'ask' | 'deny'
16
+
17
+ /** A pending approval awaiting user decision. */
18
+ export interface PendingApproval {
19
+ id: string
20
+ toolName: string
21
+ args: Record<string, any>
22
+ timestamp: number
23
+ resolve: (decision: 'approve' | 'deny') => void
24
+ }
25
+
26
+ /** Tool bundle spec — either a feature name string, or an object with filtering. */
27
+ export type ToolBundleSpec = string | {
28
+ feature: string
29
+ only?: string[]
30
+ except?: string[]
31
+ }
32
+
33
+ export const AutonomousAssistantEventsSchema = FeatureEventsSchema.extend({
34
+ started: z.tuple([]).describe('Emitted when the autonomous assistant has been initialized'),
35
+ permissionRequest: z.tuple([z.object({
36
+ id: z.string().describe('Unique approval ID'),
37
+ toolName: z.string().describe('The tool requesting permission'),
38
+ args: z.record(z.string(), z.any()).describe('The arguments the tool was called with'),
39
+ })]).describe('Emitted when a tool call requires user approval'),
40
+ permissionGranted: z.tuple([z.string().describe('Approval ID')]).describe('Emitted when a pending tool call is approved'),
41
+ permissionDenied: z.tuple([z.string().describe('Approval ID')]).describe('Emitted when a pending tool call is denied'),
42
+ toolBlocked: z.tuple([z.string().describe('Tool name'), z.string().describe('Reason')]).describe('Emitted when a tool call is blocked by deny policy'),
43
+ // Forwarded from inner assistant
44
+ chunk: z.tuple([z.string().describe('A chunk of streamed text')]).describe('Forwarded: streamed token chunk from the inner assistant'),
45
+ response: z.tuple([z.string().describe('The final response text')]).describe('Forwarded: complete response from the inner assistant'),
46
+ toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Tool arguments')]).describe('Forwarded: a tool was called'),
47
+ toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Forwarded: a tool returned a result'),
48
+ toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Forwarded: a tool call failed'),
49
+ })
50
+
51
+ export const AutonomousAssistantStateSchema = FeatureStateSchema.extend({
52
+ started: z.boolean().describe('Whether the assistant has been initialized'),
53
+ permissions: z.record(z.string(), z.enum(['allow', 'ask', 'deny'])).describe('Permission level per tool name'),
54
+ defaultPermission: z.enum(['allow', 'ask', 'deny']).describe('Permission level for tools not explicitly configured'),
55
+ pendingApprovals: z.array(z.object({
56
+ id: z.string(),
57
+ toolName: z.string(),
58
+ args: z.record(z.string(), z.any()),
59
+ timestamp: z.number(),
60
+ })).describe('Tool calls currently awaiting user approval'),
61
+ approvalHistory: z.array(z.object({
62
+ id: z.string(),
63
+ toolName: z.string(),
64
+ decision: z.enum(['approve', 'deny']),
65
+ timestamp: z.number(),
66
+ })).describe('Recent approval decisions'),
67
+ })
68
+
69
+ export const AutonomousAssistantOptionsSchema = FeatureOptionsSchema.extend({
70
+ /** Tool bundles to stack — feature names or objects with filtering. */
71
+ tools: z.array(z.union([
72
+ z.string(),
73
+ z.object({
74
+ feature: z.string(),
75
+ only: z.array(z.string()).optional(),
76
+ except: z.array(z.string()).optional(),
77
+ }),
78
+ ])).default([]).describe('Tool bundles to register on the inner assistant'),
79
+
80
+ /** Per-tool permission overrides. */
81
+ permissions: z.record(z.string(), z.enum(['allow', 'ask', 'deny'])).default({}).describe('Permission level per tool name'),
82
+
83
+ /** Default permission for tools not in the permissions map. */
84
+ defaultPermission: z.enum(['allow', 'ask', 'deny']).default('ask').describe('Default permission level for unconfigured tools'),
85
+
86
+ /** System prompt for the inner assistant. */
87
+ systemPrompt: z.string().optional().describe('System prompt for the inner assistant'),
88
+
89
+ /** Model to use. */
90
+ model: z.string().optional().describe('OpenAI model override'),
91
+
92
+ /** History mode for the inner assistant. */
93
+ historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Conversation history persistence mode'),
94
+
95
+ /** Assistant folder — if provided, loads CORE.md/tools.ts/hooks.ts from disk. */
96
+ folder: z.string().optional().describe('Assistant folder for disk-based definitions'),
97
+ })
98
+
99
+ export type AutonomousAssistantState = z.infer<typeof AutonomousAssistantStateSchema>
100
+ export type AutonomousAssistantOptions = z.infer<typeof AutonomousAssistantOptionsSchema>
101
+
102
+ /**
103
+ * An autonomous assistant that owns a lower-level Assistant instance and
104
+ * gates all tool calls through a permission system.
105
+ *
106
+ * Tools are stacked from feature bundles (fileTools, processManager, etc.)
107
+ * and each tool can be set to 'allow' (runs immediately), 'ask' (blocks
108
+ * until user approves/denies), or 'deny' (always rejected).
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const auto = container.feature('autoAssistant', {
113
+ * tools: ['fileTools', { feature: 'processManager', except: ['killAllProcesses'] }],
114
+ * permissions: {
115
+ * readFile: 'allow',
116
+ * searchFiles: 'allow',
117
+ * writeFile: 'ask',
118
+ * editFile: 'ask',
119
+ * deleteFile: 'deny',
120
+ * },
121
+ * defaultPermission: 'ask',
122
+ * systemPrompt: 'You are a coding assistant.',
123
+ * })
124
+ *
125
+ * auto.on('permissionRequest', ({ id, toolName, args }) => {
126
+ * console.log(`Tool "${toolName}" wants to run with`, args)
127
+ * // Show UI, then:
128
+ * auto.approve(id) // or auto.deny(id)
129
+ * })
130
+ *
131
+ * await auto.ask('Refactor the auth module to use async/await')
132
+ * ```
133
+ *
134
+ * @extends Feature
135
+ */
136
+ export class AutonomousAssistant extends Feature<AutonomousAssistantState, AutonomousAssistantOptions> {
137
+ static override shortcut = 'features.autoAssistant' as const
138
+ static override stateSchema = AutonomousAssistantStateSchema
139
+ static override optionsSchema = AutonomousAssistantOptionsSchema
140
+ static override eventsSchema = AutonomousAssistantEventsSchema
141
+
142
+ static { Feature.register(this, 'autoAssistant') }
143
+
144
+ /** The inner assistant instance. Created during start(). */
145
+ private _assistant: Assistant | null = null
146
+
147
+ /** Map of pending approval promises keyed by ID. */
148
+ private _pendingResolvers = new Map<string, (decision: 'approve' | 'deny') => void>()
149
+
150
+ override get initialState(): AutonomousAssistantState {
151
+ return {
152
+ ...super.initialState,
153
+ started: false,
154
+ permissions: this.options.permissions || {},
155
+ defaultPermission: this.options.defaultPermission || 'ask',
156
+ pendingApprovals: [],
157
+ approvalHistory: [],
158
+ }
159
+ }
160
+
161
+ override get container(): AGIContainer {
162
+ return super.container as AGIContainer
163
+ }
164
+
165
+ /** The inner assistant. Throws if not started. */
166
+ get assistant(): Assistant {
167
+ if (!this._assistant) throw new Error('AutonomousAssistant not started. Call start() first.')
168
+ return this._assistant
169
+ }
170
+
171
+ /** Current permission map from state. */
172
+ get permissions(): Record<string, PermissionLevel> {
173
+ return this.state.get('permissions') as Record<string, PermissionLevel>
174
+ }
175
+
176
+ /** Current pending approvals. */
177
+ get pendingApprovals(): PendingApproval[] {
178
+ const stored = this.state.get('pendingApprovals') as Array<{ id: string; toolName: string; args: Record<string, any>; timestamp: number }>
179
+ return stored.map(p => ({
180
+ ...p,
181
+ resolve: this._pendingResolvers.get(p.id) || (() => {}),
182
+ }))
183
+ }
184
+
185
+ /** Whether the assistant is started and ready. */
186
+ get isStarted(): boolean {
187
+ return this.state.get('started') as boolean
188
+ }
189
+
190
+ /** The tools registered on the inner assistant. */
191
+ get tools(): Record<string, any> {
192
+ return this._assistant?.tools || {}
193
+ }
194
+
195
+ /** The conversation on the inner assistant (if started). */
196
+ get conversation() {
197
+ return this._assistant?.conversation
198
+ }
199
+
200
+ /** Messages from the inner assistant's conversation. */
201
+ get messages() {
202
+ return this._assistant?.messages || []
203
+ }
204
+
205
+ // -------------------------------------------------------------------------
206
+ // Permission management
207
+ // -------------------------------------------------------------------------
208
+
209
+ /** Get the effective permission level for a tool. */
210
+ getPermission(toolName: string): PermissionLevel {
211
+ const perms = this.permissions
212
+ if (perms[toolName]) return perms[toolName]
213
+ return this.state.get('defaultPermission') as PermissionLevel
214
+ }
215
+
216
+ /** Set permission level for one or more tools. */
217
+ setPermission(toolName: string | string[], level: PermissionLevel): this {
218
+ const names = Array.isArray(toolName) ? toolName : [toolName]
219
+ const perms = { ...this.permissions }
220
+ for (const name of names) {
221
+ perms[name] = level
222
+ }
223
+ this.state.set('permissions', perms)
224
+ return this
225
+ }
226
+
227
+ /** Set the default permission level for unconfigured tools. */
228
+ setDefaultPermission(level: PermissionLevel): this {
229
+ this.state.set('defaultPermission', level)
230
+ return this
231
+ }
232
+
233
+ /** Allow a tool (or tools) to run without approval. */
234
+ permitTool(...toolNames: string[]): this {
235
+ return this.setPermission(toolNames, 'allow')
236
+ }
237
+
238
+ /** Require approval before a tool (or tools) can run. */
239
+ gateTool(...toolNames: string[]): this {
240
+ return this.setPermission(toolNames, 'ask')
241
+ }
242
+
243
+ /** Block a tool (or tools) from ever running. */
244
+ blockTool(...toolNames: string[]): this {
245
+ return this.setPermission(toolNames, 'deny')
246
+ }
247
+
248
+ // -------------------------------------------------------------------------
249
+ // Approval flow
250
+ // -------------------------------------------------------------------------
251
+
252
+ /** Approve a pending tool call by ID. The tool will execute. */
253
+ approve(id: string): this {
254
+ const resolver = this._pendingResolvers.get(id)
255
+ if (resolver) {
256
+ resolver('approve')
257
+ this._removePending(id)
258
+ this._recordDecision(id, 'approve')
259
+ this.emit('permissionGranted', id)
260
+ }
261
+ return this
262
+ }
263
+
264
+ /** Deny a pending tool call by ID. The tool call will be skipped. */
265
+ deny(id: string): this {
266
+ const resolver = this._pendingResolvers.get(id)
267
+ if (resolver) {
268
+ resolver('deny')
269
+ this._removePending(id)
270
+ this._recordDecision(id, 'deny')
271
+ this.emit('permissionDenied', id)
272
+ }
273
+ return this
274
+ }
275
+
276
+ /** Approve all pending tool calls. */
277
+ approveAll(): this {
278
+ for (const { id } of this.pendingApprovals) {
279
+ this.approve(id)
280
+ }
281
+ return this
282
+ }
283
+
284
+ /** Deny all pending tool calls. */
285
+ denyAll(): this {
286
+ for (const { id } of this.pendingApprovals) {
287
+ this.deny(id)
288
+ }
289
+ return this
290
+ }
291
+
292
+ // -------------------------------------------------------------------------
293
+ // Lifecycle
294
+ // -------------------------------------------------------------------------
295
+
296
+ /**
297
+ * Initialize the inner assistant, stack tool bundles, and wire up
298
+ * the permission interceptor.
299
+ */
300
+ async start(): Promise<this> {
301
+ if (this.isStarted) return this
302
+
303
+ // Create the inner assistant
304
+ const assistantOpts: Record<string, any> = {}
305
+ if (this.options.systemPrompt) assistantOpts.systemPrompt = this.options.systemPrompt
306
+ if (this.options.model) assistantOpts.model = this.options.model
307
+ if (this.options.historyMode) assistantOpts.historyMode = this.options.historyMode
308
+ if (this.options.folder) assistantOpts.folder = this.options.folder
309
+
310
+ this._assistant = this.container.feature('assistant', assistantOpts)
311
+
312
+ // Stack tool bundles
313
+ for (const spec of this.options.tools) {
314
+ this._stackToolBundle(spec)
315
+ }
316
+
317
+ // Wire the permission interceptor
318
+ this._assistant.intercept('beforeToolCall', async (ctx: ToolCallCtx, next: () => Promise<void>) => {
319
+ const policy = this.getPermission(ctx.name)
320
+
321
+ if (policy === 'deny') {
322
+ ctx.skip = true
323
+ ctx.result = JSON.stringify({ blocked: true, tool: ctx.name, reason: 'Permission denied by policy.' })
324
+ this.emit('toolBlocked', ctx.name, 'deny policy')
325
+ return
326
+ }
327
+
328
+ if (policy === 'allow') {
329
+ await next()
330
+ return
331
+ }
332
+
333
+ // 'ask' — block until user decides
334
+ const decision = await this._requestApproval(ctx.name, ctx.args)
335
+
336
+ if (decision === 'approve') {
337
+ await next()
338
+ } else {
339
+ ctx.skip = true
340
+ ctx.result = JSON.stringify({ blocked: true, tool: ctx.name, reason: 'User denied this action.' })
341
+ }
342
+ })
343
+
344
+ // Forward events from inner assistant
345
+ this._assistant.on('chunk', (text: string) => this.emit('chunk', text))
346
+ this._assistant.on('response', (text: string) => this.emit('response', text))
347
+ this._assistant.on('toolCall', (name: string, args: any) => this.emit('toolCall', name, args))
348
+ this._assistant.on('toolResult', (name: string, result: any) => this.emit('toolResult', name, result))
349
+ this._assistant.on('toolError', (name: string, error: any) => this.emit('toolError', name, error))
350
+
351
+ // Start the inner assistant
352
+ await this._assistant.start()
353
+
354
+ this.state.set('started', true)
355
+ this.emit('started')
356
+
357
+ return this
358
+ }
359
+
360
+ /**
361
+ * Ask the autonomous assistant a question. Auto-starts if needed.
362
+ * Tool calls will be gated by the permission system.
363
+ */
364
+ async ask(question: string, options?: Record<string, any>): Promise<string> {
365
+ if (!this.isStarted) await this.start()
366
+ return this.assistant.ask(question, options)
367
+ }
368
+
369
+ /**
370
+ * Add a tool bundle after initialization. Useful for dynamically
371
+ * extending the assistant's capabilities.
372
+ */
373
+ use(spec: ToolBundleSpec): this {
374
+ this._stackToolBundle(spec)
375
+ return this
376
+ }
377
+
378
+ // -------------------------------------------------------------------------
379
+ // Internal
380
+ // -------------------------------------------------------------------------
381
+
382
+ /** Resolve a tool bundle spec and register its tools on the inner assistant. */
383
+ private _stackToolBundle(spec: ToolBundleSpec): void {
384
+ if (!this._assistant) throw new Error('Cannot stack tools before start()')
385
+
386
+ const featureName = typeof spec === 'string' ? spec : spec.feature
387
+ const filterOpts = typeof spec === 'string' ? undefined : {
388
+ only: spec.only,
389
+ except: spec.except,
390
+ }
391
+
392
+ const feature = this.container.feature(featureName as any)
393
+ const tools = (feature as any).toTools(filterOpts)
394
+ this._assistant.use(tools)
395
+ }
396
+
397
+ /** Create a pending approval, emit the event, and return a promise that resolves with the decision. */
398
+ private _requestApproval(toolName: string, args: Record<string, any>): Promise<'approve' | 'deny'> {
399
+ const id = this.container.utils.uuid()
400
+
401
+ return new Promise<'approve' | 'deny'>((resolve) => {
402
+ this._pendingResolvers.set(id, resolve)
403
+
404
+ const pending = [...(this.state.get('pendingApprovals') as any[])]
405
+ pending.push({ id, toolName, args, timestamp: Date.now() })
406
+ this.state.set('pendingApprovals', pending)
407
+
408
+ this.emit('permissionRequest', { id, toolName, args })
409
+ })
410
+ }
411
+
412
+ /** Remove a pending approval from state. */
413
+ private _removePending(id: string): void {
414
+ this._pendingResolvers.delete(id)
415
+ const pending = (this.state.get('pendingApprovals') as any[]).filter((p: any) => p.id !== id)
416
+ this.state.set('pendingApprovals', pending)
417
+ }
418
+
419
+ /** Record a decision in the approval history. */
420
+ private _recordDecision(id: string, decision: 'approve' | 'deny'): void {
421
+ const pending = (this.state.get('pendingApprovals') as any[]).find((p: any) => p.id === id)
422
+ const history = [...(this.state.get('approvalHistory') as any[])]
423
+ history.push({
424
+ id,
425
+ toolName: pending?.toolName || 'unknown',
426
+ decision,
427
+ timestamp: Date.now(),
428
+ })
429
+ // Keep last 100 entries
430
+ if (history.length > 100) history.splice(0, history.length - 100)
431
+ this.state.set('approvalHistory', history)
432
+ }
433
+ }
434
+
435
+ export default AutonomousAssistant
@@ -64,7 +64,20 @@ export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
64
64
  local: z.boolean().optional().describe('Whether to use the local ollama models instead of the remote OpenAI models'),
65
65
 
66
66
  /** Maximum number of output tokens per completion */
67
- maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
67
+ maxTokens: z.number().optional().describe('Maximum number of output tokens per completion (default 512)'),
68
+
69
+ /** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
70
+ temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2). Higher = more random, lower = more deterministic'),
71
+ /** Nucleus sampling: only consider tokens with top_p cumulative probability (0-1). */
72
+ topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1). Lower = more focused'),
73
+ /** Top-K sampling: only consider the K most likely tokens. Not supported by OpenAI — used with local/Anthropic models. */
74
+ topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
75
+ /** Penalizes tokens based on how often they already appeared (-2 to 2). */
76
+ frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2). Positive = discourage repetition'),
77
+ /** Penalizes tokens based on whether they appeared at all (-2 to 2). */
78
+ presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2). Positive = encourage new topics'),
79
+ /** Stop sequences — model stops generating when it encounters any of these strings. */
80
+ stop: z.array(z.string()).optional().describe('Stop sequences — generation halts when any of these strings is produced'),
68
81
 
69
82
  /** Enable automatic compaction when estimated input tokens approach the context limit */
70
83
  autoCompact: z.boolean().optional().describe('Enable automatic compaction when input tokens approach the context limit'),
@@ -199,9 +212,9 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
199
212
  /** The active structured output schema for the current ask() call, if any. */
200
213
  private _activeSchema: z.ZodType | null = null
201
214
 
202
- /** Resolved max tokens: per-call override > options-level > undefined (no limit). */
215
+ /** Resolved max tokens: per-call override > options-level > default 512. */
203
216
  private get maxTokens(): number | undefined {
204
- return (this.state.get('callMaxTokens') as number | null) ?? this.options.maxTokens ?? undefined
217
+ return (this.state.get('callMaxTokens') as number | null) ?? this.options.maxTokens ?? 512
205
218
  }
206
219
 
207
220
  /** @returns Default state seeded from options: id, thread, model, initial history, and zero token usage. */
@@ -290,6 +303,26 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
290
303
  return !!this.state.get('streaming')
291
304
  }
292
305
 
306
+ /**
307
+ * Returns the correct parameter name for limiting output tokens.
308
+ * Local models (LM Studio, Ollama) and legacy OpenAI models use max_tokens.
309
+ * Newer OpenAI models (gpt-4o+, gpt-4.1, gpt-5, o1, o3, o4) require max_completion_tokens.
310
+ */
311
+ private get maxTokensParam(): 'max_tokens' | 'max_completion_tokens' {
312
+ if (this.options.local) return 'max_tokens'
313
+
314
+ const model = this.model
315
+ const needsCompletionTokens = [
316
+ 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4',
317
+ ]
318
+
319
+ if (needsCompletionTokens.some((prefix) => model.startsWith(prefix))) {
320
+ return 'max_completion_tokens'
321
+ }
322
+
323
+ return 'max_tokens'
324
+ }
325
+
293
326
  /** The context window size for the current model (from options override or auto-detected). */
294
327
  get contextWindow(): number {
295
328
  return this.options.contextWindow || getContextWindow(this.model)
@@ -705,13 +738,20 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
705
738
  return result
706
739
  }
707
740
 
741
+ let args: Record<string, any>
742
+ try {
743
+ args = rawArgs ? JSON.parse(rawArgs) : {}
744
+ } catch (parseErr: any) {
745
+ const result = JSON.stringify({ error: `Failed to parse tool arguments: ${parseErr.message}`, rawArgs })
746
+ this.emit('toolError', toolName, parseErr)
747
+ return result
748
+ }
749
+
708
750
  if (this.toolExecutor) {
709
- const args = rawArgs ? JSON.parse(rawArgs) : {}
710
751
  return this.toolExecutor(toolName, args, tool.handler)
711
752
  }
712
753
 
713
754
  try {
714
- const args = rawArgs ? JSON.parse(rawArgs) : {}
715
755
  this.emit('toolCall', toolName, args)
716
756
  const output = await tool.handler(args)
717
757
  const result = typeof output === 'string' ? output : JSON.stringify(output)
@@ -759,6 +799,12 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
759
799
  ...(toolsParam ? { tools: toolsParam, tool_choice: 'auto', parallel_tool_calls: true } : {}),
760
800
  ...(this.responsesInstructions ? { instructions: this.responsesInstructions } : {}),
761
801
  ...(this.maxTokens ? { max_output_tokens: this.maxTokens } : {}),
802
+ ...(this.options.temperature != null ? { temperature: this.options.temperature } : {}),
803
+ ...(this.options.topP != null ? { top_p: this.options.topP } : {}),
804
+ ...(this.options.topK != null ? { top_k: this.options.topK } : {}),
805
+ ...(this.options.frequencyPenalty != null ? { frequency_penalty: this.options.frequencyPenalty } : {}),
806
+ ...(this.options.presencePenalty != null ? { presence_penalty: this.options.presencePenalty } : {}),
807
+ ...(this.options.stop ? { stop: this.options.stop } : {}),
762
808
  ...textFormat,
763
809
  })
764
810
 
@@ -901,7 +947,13 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
901
947
  messages: this.messages,
902
948
  stream: true,
903
949
  ...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
904
- ...(this.maxTokens ? { max_tokens: this.maxTokens } : {}),
950
+ ...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
951
+ ...(this.options.temperature != null ? { temperature: this.options.temperature } : {}),
952
+ ...(this.options.topP != null ? { top_p: this.options.topP } : {}),
953
+ ...(this.options.topK != null ? { top_k: this.options.topK } : {}),
954
+ ...(this.options.frequencyPenalty != null ? { frequency_penalty: this.options.frequencyPenalty } : {}),
955
+ ...(this.options.presencePenalty != null ? { presence_penalty: this.options.presencePenalty } : {}),
956
+ ...(this.options.stop ? { stop: this.options.stop } : {}),
905
957
  ...responseFormat,
906
958
  })
907
959