@soederpop/luca 0.0.32 → 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.
- package/README.md +241 -36
- package/bun.lock +24 -5
- package/commands/build-python-bridge.ts +43 -0
- package/docs/apis/clients/rest.md +7 -7
- package/docs/apis/clients/websocket.md +23 -10
- package/docs/apis/features/agi/assistant.md +155 -8
- package/docs/apis/features/agi/assistants-manager.md +90 -22
- package/docs/apis/features/agi/auto-assistant.md +377 -0
- package/docs/apis/features/agi/browser-use.md +802 -0
- package/docs/apis/features/agi/claude-code.md +6 -1
- package/docs/apis/features/agi/conversation-history.md +7 -6
- package/docs/apis/features/agi/conversation.md +111 -38
- package/docs/apis/features/agi/docs-reader.md +35 -57
- package/docs/apis/features/agi/file-tools.md +163 -0
- package/docs/apis/features/agi/openapi.md +2 -2
- package/docs/apis/features/agi/skills-library.md +227 -0
- package/docs/apis/features/node/content-db.md +125 -4
- package/docs/apis/features/node/disk-cache.md +11 -11
- package/docs/apis/features/node/downloader.md +1 -1
- package/docs/apis/features/node/file-manager.md +15 -15
- package/docs/apis/features/node/fs.md +78 -21
- package/docs/apis/features/node/git.md +50 -10
- package/docs/apis/features/node/google-calendar.md +3 -0
- package/docs/apis/features/node/google-docs.md +10 -1
- package/docs/apis/features/node/google-drive.md +3 -0
- package/docs/apis/features/node/google-mail.md +214 -0
- package/docs/apis/features/node/google-sheets.md +3 -0
- package/docs/apis/features/node/ink.md +10 -10
- package/docs/apis/features/node/ipc-socket.md +83 -93
- package/docs/apis/features/node/networking.md +5 -5
- package/docs/apis/features/node/os.md +7 -7
- package/docs/apis/features/node/package-finder.md +14 -14
- package/docs/apis/features/node/proc.md +2 -1
- package/docs/apis/features/node/process-manager.md +70 -3
- package/docs/apis/features/node/python.md +265 -9
- package/docs/apis/features/node/redis.md +380 -0
- package/docs/apis/features/node/ui.md +13 -13
- package/docs/apis/servers/express.md +35 -7
- package/docs/apis/servers/mcp.md +3 -3
- package/docs/apis/servers/websocket.md +51 -8
- package/docs/bootstrap/CLAUDE.md +1 -1
- package/docs/bootstrap/SKILL.md +93 -7
- package/docs/examples/feature-as-tool-provider.md +143 -0
- package/docs/examples/python.md +42 -1
- package/docs/introspection.md +15 -5
- package/docs/tutorials/00-bootstrap.md +3 -3
- package/docs/tutorials/02-container.md +2 -2
- package/docs/tutorials/10-creating-features.md +5 -0
- package/docs/tutorials/13-introspection.md +12 -2
- package/docs/tutorials/19-python-sessions.md +401 -0
- package/package.json +8 -4
- package/src/agi/container.server.ts +8 -0
- package/src/agi/features/assistant.ts +18 -0
- package/src/agi/features/autonomous-assistant.ts +435 -0
- package/src/agi/features/conversation.ts +58 -6
- package/src/agi/features/file-tools.ts +286 -0
- package/src/agi/features/luca-coder.ts +643 -0
- package/src/bootstrap/generated.ts +705 -17
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +22 -13
- package/src/commands/bootstrap.ts +49 -6
- package/src/commands/code.ts +369 -0
- package/src/commands/describe.ts +7 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/sandbox-mcp.ts +7 -7
- package/src/commands/save-api-docs.ts +1 -1
- package/src/container-describer.ts +4 -4
- package/src/container.ts +10 -19
- package/src/helper.ts +24 -33
- package/src/introspection/generated.agi.ts +3026 -590
- package/src/introspection/generated.node.ts +1625 -688
- package/src/introspection/generated.web.ts +15 -57
- package/src/node/container.ts +5 -0
- package/src/node/features/figlet-fonts.ts +597 -0
- package/src/node/features/fs.ts +3 -9
- package/src/node/features/helpers.ts +20 -0
- package/src/node/features/python.ts +429 -16
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/ui.ts +4 -11
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +227 -0
- package/src/scaffolds/generated.ts +1 -1
- package/test/python-session.test.ts +105 -0
- package/assistants/lucaExpert/CORE.md +0 -37
- package/assistants/lucaExpert/hooks.ts +0 -9
- 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 >
|
|
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 ??
|
|
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 ? {
|
|
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
|
|