@soederpop/luca 0.2.1 → 0.2.3
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/.github/workflows/release.yaml +2 -0
- package/CNAME +1 -0
- package/assistants/codingAssistant/ABOUT.md +3 -1
- package/assistants/codingAssistant/CORE.md +2 -4
- package/assistants/codingAssistant/hooks.ts +9 -10
- package/assistants/codingAssistant/tools.ts +9 -0
- package/assistants/inkbot/ABOUT.md +13 -2
- package/assistants/inkbot/CORE.md +278 -39
- package/assistants/inkbot/hooks.ts +0 -8
- package/assistants/inkbot/tools.ts +24 -18
- package/assistants/researcher/ABOUT.md +5 -0
- package/assistants/researcher/CORE.md +46 -0
- package/assistants/researcher/hooks.ts +16 -0
- package/assistants/researcher/tools.ts +237 -0
- package/commands/inkbot.ts +526 -194
- package/docs/CNAME +1 -0
- package/docs/examples/assistant-hooks-reference.ts +171 -0
- package/index.html +1430 -0
- package/package.json +1 -1
- package/public/slides-ai-native.html +902 -0
- package/public/slides-intro.html +974 -0
- package/src/agi/features/assistant.ts +432 -62
- package/src/agi/features/conversation.ts +170 -10
- package/src/bootstrap/generated.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/helper.ts +12 -3
- package/src/introspection/generated.agi.ts +1663 -644
- package/src/introspection/generated.node.ts +1637 -870
- package/src/introspection/generated.web.ts +1 -1
- package/src/python/generated.ts +1 -1
- package/src/scaffolds/generated.ts +1 -1
- package/test/assistant-hooks.test.ts +306 -0
- package/test/assistant.test.ts +1 -1
- package/test/fork-and-research.test.ts +450 -0
- package/SPEC.md +0 -304
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const fileTools = container.feature('fileTools')
|
|
4
|
+
|
|
5
|
+
export const use = [
|
|
6
|
+
container.feature('browserUse', { headed: true }),
|
|
7
|
+
fileTools.toTools({ only: ['listDirectory', 'writeFile', 'editFile', 'deleteFile'] }),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export const schemas = {
|
|
11
|
+
createResearchJob: z.object({
|
|
12
|
+
prompt: z.string().describe(
|
|
13
|
+
'Shared context that all parallel research forks will see. Describe the overarching question or framing so each fork understands the bigger picture. Keep it concise but sufficient — each fork only sees this prompt plus its individual question.'
|
|
14
|
+
),
|
|
15
|
+
questions: z.array(z.string().min(5)).min(2).describe(
|
|
16
|
+
'The specific questions to investigate in parallel. Each one becomes an independent research fork. Make them focused and non-overlapping — broad questions produce shallow results. 2-5 questions is the sweet spot.'
|
|
17
|
+
),
|
|
18
|
+
effort: z.enum(['low', 'medium', 'high']).optional().describe(
|
|
19
|
+
'Research effort level. "low" for quick factual lookups and simple verification. "medium" (default) for standard research. "high" for deep analysis requiring nuanced reasoning.'
|
|
20
|
+
),
|
|
21
|
+
history: z.enum(['full', 'none']).optional().describe(
|
|
22
|
+
'How much conversation context each fork inherits. "none" (default) means forks only get the system prompt + shared research prompt — cheapest and usually sufficient. "full" means forks see the entire conversation so far — use when the question requires understanding prior discussion.'
|
|
23
|
+
),
|
|
24
|
+
}).describe(
|
|
25
|
+
'Kick off a parallel research job. Creates multiple independent forks that investigate different angles simultaneously. Returns a job ID immediately — does NOT block. Use checkResearchJobs to poll for results. Best for 2-5 independent sub-questions that do not depend on each other.'
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
checkResearchJobs: z.object({
|
|
29
|
+
jobId: z.string().optional().describe(
|
|
30
|
+
'Check a specific job by ID. Omit to get a summary of all active and completed jobs.'
|
|
31
|
+
),
|
|
32
|
+
}).describe(
|
|
33
|
+
'Check the status and results of research jobs. Returns progress (completed/total), status, and any results that have come in so far. Call this periodically after creating a job — do not spin-wait.'
|
|
34
|
+
),
|
|
35
|
+
|
|
36
|
+
addSource: z.object({
|
|
37
|
+
url: z.string().describe('The URL of the source. Use the actual page URL, not a shortened or tracking link.'),
|
|
38
|
+
title: z.string().describe('A concise title for the source. Use the page title or a descriptive label if the page title is generic.'),
|
|
39
|
+
comment: z.string().describe(
|
|
40
|
+
'Your annotation on this source — what it establishes, why it matters, how reliable you judge it to be. This is YOUR note, not a summary of the source. Be specific: "Confirms that X uses Y approach as of 2024" is better than "Discusses X".'
|
|
41
|
+
),
|
|
42
|
+
tags: z.array(z.string()).optional().describe(
|
|
43
|
+
'Optional tags for categorizing this source. Useful when a research project spans multiple sub-topics.'
|
|
44
|
+
),
|
|
45
|
+
}).describe(
|
|
46
|
+
'Register a source you have found during research. Every meaningful claim should trace back to a source. Call this as soon as you find something relevant — do not wait until the end. Returns a source ID you can use in citations.'
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
removeSource: z.object({
|
|
50
|
+
sourceId: z.string().describe('The ID of the source to remove (returned by addSource).'),
|
|
51
|
+
reason: z.string().optional().describe(
|
|
52
|
+
'Why you are removing this source. Helps maintain an audit trail — e.g. "Superseded by more recent data" or "Source turned out to be unreliable".'
|
|
53
|
+
),
|
|
54
|
+
}).describe(
|
|
55
|
+
'Remove a previously registered source. Use when a source turns out to be unreliable, outdated, irrelevant, or superseded by a better source. The source is soft-deleted — it remains in the audit trail but will not appear in active sources.'
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
listSources: z.object({
|
|
59
|
+
tags: z.array(z.string()).optional().describe('Filter sources by tags. Returns only sources matching ALL specified tags.'),
|
|
60
|
+
}).describe(
|
|
61
|
+
'List all active sources registered during this research session. Returns source IDs, titles, URLs, and your comments. Use this to review what you have found so far and to construct citation lists.'
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -- Source Management --
|
|
66
|
+
|
|
67
|
+
interface Source {
|
|
68
|
+
id: string
|
|
69
|
+
url: string
|
|
70
|
+
title: string
|
|
71
|
+
comment: string
|
|
72
|
+
tags: string[]
|
|
73
|
+
addedAt: string
|
|
74
|
+
removed?: boolean
|
|
75
|
+
removedReason?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSources(): Source[] {
|
|
79
|
+
return (assistant.state.get('sources') as Source[] | undefined) || []
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setSources(sources: Source[]) {
|
|
83
|
+
assistant.state.set('sources', sources)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function addSource(options: z.infer<typeof schemas.addSource>): string {
|
|
87
|
+
const sources = getSources()
|
|
88
|
+
const id = String(sources.length + 1)
|
|
89
|
+
|
|
90
|
+
const source: Source = {
|
|
91
|
+
id,
|
|
92
|
+
url: options.url,
|
|
93
|
+
title: options.title,
|
|
94
|
+
comment: options.comment,
|
|
95
|
+
tags: options.tags || [],
|
|
96
|
+
addedAt: new Date().toISOString(),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setSources([...sources, source])
|
|
100
|
+
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
sourceId: id,
|
|
103
|
+
message: `Source [${id}] registered: "${options.title}"`,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function removeSource(options: z.infer<typeof schemas.removeSource>): string {
|
|
108
|
+
const sources = getSources()
|
|
109
|
+
const source = sources.find(s => s.id === options.sourceId && !s.removed)
|
|
110
|
+
|
|
111
|
+
if (!source) {
|
|
112
|
+
return JSON.stringify({ error: `Source ${options.sourceId} not found or already removed.` })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
source.removed = true
|
|
116
|
+
source.removedReason = options.reason
|
|
117
|
+
setSources([...sources])
|
|
118
|
+
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
message: `Source [${source.id}] removed: "${source.title}"${options.reason ? ` — ${options.reason}` : ''}`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function listSources(options: z.infer<typeof schemas.listSources>): string {
|
|
125
|
+
const sources = getSources().filter(s => !s.removed)
|
|
126
|
+
|
|
127
|
+
const filtered = options.tags?.length
|
|
128
|
+
? sources.filter(s => options.tags!.every(t => s.tags.includes(t)))
|
|
129
|
+
: sources
|
|
130
|
+
|
|
131
|
+
if (filtered.length === 0) {
|
|
132
|
+
return JSON.stringify({ sources: [], message: 'No active sources registered.' })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return JSON.stringify({
|
|
136
|
+
count: filtered.length,
|
|
137
|
+
sources: filtered.map(s => ({
|
|
138
|
+
id: s.id,
|
|
139
|
+
title: s.title,
|
|
140
|
+
url: s.url,
|
|
141
|
+
comment: s.comment,
|
|
142
|
+
tags: s.tags,
|
|
143
|
+
})),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -- Research Job Management --
|
|
148
|
+
|
|
149
|
+
const effortModels: Record<string, string> = {
|
|
150
|
+
low: 'gpt-5.4',
|
|
151
|
+
medium: 'gpt-5.4',
|
|
152
|
+
high: 'gpt-5.4',
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function createResearchJob(options: z.infer<typeof schemas.createResearchJob>): Promise<string> {
|
|
156
|
+
if (assistant.isFork) {
|
|
157
|
+
return JSON.stringify({ error: 'Research forks cannot create sub-forks. Answer the question directly.' })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const model = effortModels[options.effort || 'medium']
|
|
161
|
+
|
|
162
|
+
const outputFolder = assistant.state.get('outputFolder') as string | undefined
|
|
163
|
+
|
|
164
|
+
if (outputFolder) {
|
|
165
|
+
await container.fs.ensureFolderAsync(outputFolder)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const job = await assistant.createResearchJob(
|
|
169
|
+
options.prompt,
|
|
170
|
+
options.questions,
|
|
171
|
+
{
|
|
172
|
+
history: options.history === 'full' ? 'full' : 'none',
|
|
173
|
+
model,
|
|
174
|
+
forbidTools: ['createResearchJob', 'checkResearchJobs'],
|
|
175
|
+
onFork: (fork) => {
|
|
176
|
+
if (outputFolder) {
|
|
177
|
+
fork.addSystemPromptExtension('output-folder', [
|
|
178
|
+
'## Output Directory',
|
|
179
|
+
`Write ALL research output files to: ${outputFolder}/`,
|
|
180
|
+
'Do NOT write files outside of this directory.',
|
|
181
|
+
'',
|
|
182
|
+
'## Incremental Saving',
|
|
183
|
+
'Save your work to disk incrementally — create your output file early with your first finding, then use editFile to append new sections as you discover more.',
|
|
184
|
+
'Do NOT wait until you are finished to write. Partial results must survive interruption.',
|
|
185
|
+
].join('\n'))
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return JSON.stringify({
|
|
192
|
+
jobId: job.id,
|
|
193
|
+
total: options.questions.length,
|
|
194
|
+
status: 'running',
|
|
195
|
+
message: `Research job started with ${options.questions.length} parallel forks. Use checkResearchJobs with jobId "${job.id}" to monitor progress.`,
|
|
196
|
+
questions: options.questions,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function checkResearchJobs(options: z.infer<typeof schemas.checkResearchJobs>): string {
|
|
201
|
+
if (options.jobId) {
|
|
202
|
+
const job = assistant.researchJobs.get(options.jobId)
|
|
203
|
+
if (!job) {
|
|
204
|
+
return JSON.stringify({ error: `No job found with ID "${options.jobId}".` })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const state = {
|
|
208
|
+
jobId: job.id,
|
|
209
|
+
status: job.state.get('status'),
|
|
210
|
+
completed: job.state.get('completed'),
|
|
211
|
+
total: job.state.get('total'),
|
|
212
|
+
questions: job.state.get('questions'),
|
|
213
|
+
results: job.state.get('results'),
|
|
214
|
+
errors: job.state.get('errors'),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return JSON.stringify(state)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Summary of all jobs
|
|
221
|
+
const jobs = Array.from(assistant.researchJobs.entries()).map(([id, job]) => ({
|
|
222
|
+
jobId: id,
|
|
223
|
+
status: job.state.get('status'),
|
|
224
|
+
completed: job.state.get('completed'),
|
|
225
|
+
total: job.state.get('total'),
|
|
226
|
+
questions: job.state.get('questions'),
|
|
227
|
+
}))
|
|
228
|
+
|
|
229
|
+
if (jobs.length === 0) {
|
|
230
|
+
return JSON.stringify({ jobs: [], message: 'No research jobs have been created.' })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
count: jobs.length,
|
|
235
|
+
jobs,
|
|
236
|
+
})
|
|
237
|
+
}
|