@soederpop/luca 0.0.32 → 0.0.35
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 -6
- package/commands/build-python-bridge.ts +43 -0
- package/docs/README.md +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -1
- 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 -5
- package/scripts/examples/using-assistant-with-mcp.ts +2 -7
- package/scripts/test-linux-binary.sh +80 -0
- 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 -107
- 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 -849
- package/src/introspection/generated.node.ts +1690 -1012
- package/src/introspection/generated.web.ts +15 -57
- package/src/node/container.ts +5 -5
- 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
- package/docs/examples/port-exposer.md +0 -89
- package/src/node/features/port-exposer.ts +0 -351
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# CI-style test: cross-compile luca for Linux and verify it runs in a Docker container
|
|
5
|
+
# Usage: bash scripts/test-linux-binary.sh
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
9
|
+
DIST_DIR="$PROJECT_DIR/dist"
|
|
10
|
+
LINUX_BINARY="$DIST_DIR/luca-linux"
|
|
11
|
+
|
|
12
|
+
echo "=== Cross-compiling luca for linux-arm64 ==="
|
|
13
|
+
cd "$PROJECT_DIR"
|
|
14
|
+
|
|
15
|
+
# Run the pre-compile build steps (introspection, scaffolds, etc.)
|
|
16
|
+
bun run build:introspection
|
|
17
|
+
bun run build:scaffolds
|
|
18
|
+
bun run build:bootstrap
|
|
19
|
+
bun run build:python-bridge
|
|
20
|
+
bash scripts/stamp-build.sh
|
|
21
|
+
|
|
22
|
+
# Cross-compile for linux arm64 (matches Docker on Apple Silicon)
|
|
23
|
+
bun build ./src/cli/cli.ts --compile --target=bun-linux-arm64 --outfile "$LINUX_BINARY" --external node-llama-cpp
|
|
24
|
+
|
|
25
|
+
echo ""
|
|
26
|
+
echo "=== Built linux binary: $(file "$LINUX_BINARY") ==="
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# Create a minimal Dockerfile inline
|
|
30
|
+
DOCKER_TAG="luca-linux-test"
|
|
31
|
+
|
|
32
|
+
docker build -t "$DOCKER_TAG" -f - "$DIST_DIR" <<'DOCKERFILE'
|
|
33
|
+
FROM debian:bookworm-slim
|
|
34
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
35
|
+
WORKDIR /app
|
|
36
|
+
COPY luca-linux /usr/local/bin/luca
|
|
37
|
+
RUN chmod +x /usr/local/bin/luca
|
|
38
|
+
DOCKERFILE
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo "=== Running smoke tests in Docker container ==="
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
PASS=0
|
|
45
|
+
FAIL=0
|
|
46
|
+
|
|
47
|
+
run_test() {
|
|
48
|
+
local description="$1"
|
|
49
|
+
shift
|
|
50
|
+
echo -n " TEST: $description ... "
|
|
51
|
+
if output=$(docker run --rm "$DOCKER_TAG" "$@" 2>&1); then
|
|
52
|
+
echo "PASS"
|
|
53
|
+
PASS=$((PASS + 1))
|
|
54
|
+
else
|
|
55
|
+
echo "FAIL"
|
|
56
|
+
echo " Output: $output"
|
|
57
|
+
FAIL=$((FAIL + 1))
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Basic smoke tests
|
|
62
|
+
run_test "binary executes" luca --version
|
|
63
|
+
run_test "help flag works" luca --help
|
|
64
|
+
run_test "eval runs JS expression" luca eval "1 + 1"
|
|
65
|
+
run_test "describe features" luca describe features
|
|
66
|
+
run_test "container basics via eval" luca eval "container.uuid"
|
|
67
|
+
|
|
68
|
+
echo ""
|
|
69
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
70
|
+
|
|
71
|
+
# Cleanup
|
|
72
|
+
docker rmi "$DOCKER_TAG" > /dev/null 2>&1 || true
|
|
73
|
+
rm -f "$LINUX_BINARY"
|
|
74
|
+
|
|
75
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
76
|
+
echo "FAILED"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "ALL TESTS PASSED"
|
|
@@ -14,6 +14,8 @@ import { SkillsLibrary } from './features/skills-library'
|
|
|
14
14
|
import { BrowserUse } from './features/browser-use'
|
|
15
15
|
import { SemanticSearch } from '@soederpop/luca/node/features/semantic-search'
|
|
16
16
|
import { ContentDb } from '@soederpop/luca/node/features/content-db'
|
|
17
|
+
import { FileTools } from './features/file-tools'
|
|
18
|
+
import { LucaCoder } from './features/luca-coder'
|
|
17
19
|
|
|
18
20
|
import type { ConversationTool } from './features/conversation'
|
|
19
21
|
import type { ZodType } from 'zod'
|
|
@@ -28,6 +30,8 @@ export {
|
|
|
28
30
|
DocsReader,
|
|
29
31
|
SkillsLibrary,
|
|
30
32
|
BrowserUse,
|
|
33
|
+
FileTools,
|
|
34
|
+
LucaCoder,
|
|
31
35
|
SemanticSearch,
|
|
32
36
|
ContentDb,
|
|
33
37
|
NodeContainer,
|
|
@@ -52,6 +56,8 @@ export interface AGIFeatures extends NodeFeatures {
|
|
|
52
56
|
docsReader: typeof DocsReader
|
|
53
57
|
skillsLibrary: typeof SkillsLibrary
|
|
54
58
|
browserUse: typeof BrowserUse
|
|
59
|
+
fileTools: typeof FileTools
|
|
60
|
+
lucaCoder: typeof LucaCoder
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export interface ConversationFactoryOptions {
|
|
@@ -125,6 +131,8 @@ const container = new AGIContainer()
|
|
|
125
131
|
.use(DocsReader)
|
|
126
132
|
.use(SkillsLibrary)
|
|
127
133
|
.use(BrowserUse)
|
|
134
|
+
.use(FileTools)
|
|
135
|
+
.use(LucaCoder)
|
|
128
136
|
.use(SemanticSearch)
|
|
129
137
|
|
|
130
138
|
container.docs = container.feature('contentDb', {
|
|
@@ -79,6 +79,18 @@ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
79
79
|
/** Maximum number of output tokens per completion */
|
|
80
80
|
|
|
81
81
|
maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
|
|
82
|
+
/** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
|
|
83
|
+
temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2)'),
|
|
84
|
+
/** Nucleus sampling cutoff (0-1). */
|
|
85
|
+
topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1)'),
|
|
86
|
+
/** Top-K sampling. Only supported by local/Anthropic models. */
|
|
87
|
+
topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
|
|
88
|
+
/** Frequency penalty (-2 to 2). */
|
|
89
|
+
frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2)'),
|
|
90
|
+
/** Presence penalty (-2 to 2). */
|
|
91
|
+
presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2)'),
|
|
92
|
+
/** Stop sequences. */
|
|
93
|
+
stop: z.array(z.string()).optional().describe('Stop sequences'),
|
|
82
94
|
|
|
83
95
|
local: z.boolean().default(false).describe('Whether to use our local models for this'),
|
|
84
96
|
|
|
@@ -261,6 +273,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
261
273
|
tools: this.tools,
|
|
262
274
|
api: 'chat',
|
|
263
275
|
...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
|
|
276
|
+
...(this.options.temperature != null ? { temperature: this.options.temperature } : {}),
|
|
277
|
+
...(this.options.topP != null ? { topP: this.options.topP } : {}),
|
|
278
|
+
...(this.options.topK != null ? { topK: this.options.topK } : {}),
|
|
279
|
+
...(this.options.frequencyPenalty != null ? { frequencyPenalty: this.options.frequencyPenalty } : {}),
|
|
280
|
+
...(this.options.presencePenalty != null ? { presencePenalty: this.options.presencePenalty } : {}),
|
|
281
|
+
...(this.options.stop ? { stop: this.options.stop } : {}),
|
|
264
282
|
history: [
|
|
265
283
|
{ role: 'system', content: this.effectiveSystemPrompt },
|
|
266
284
|
],
|
|
@@ -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
|