@soederpop/luca 0.0.28 → 0.0.30
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/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/examples/structured-output-with-assistants.md +144 -0
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/package.json +1 -1
- package/src/agi/container.server.ts +4 -0
- package/src/agi/features/assistant.ts +132 -2
- package/src/agi/features/browser-use.ts +623 -0
- package/src/agi/features/conversation.ts +135 -45
- package/src/agi/lib/interceptor-chain.ts +79 -0
- package/src/bootstrap/generated.ts +381 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/container.ts +411 -113
- package/src/helper.ts +189 -5
- package/src/introspection/generated.agi.ts +17664 -11568
- package/src/introspection/generated.node.ts +4891 -1860
- package/src/introspection/generated.web.ts +379 -291
- package/src/introspection/index.ts +7 -0
- package/src/introspection/scan.ts +224 -7
- package/src/node/container.ts +31 -10
- package/src/node/features/content-db.ts +7 -7
- package/src/node/features/disk-cache.ts +11 -11
- package/src/node/features/esbuild.ts +3 -3
- package/src/node/features/file-manager.ts +37 -16
- package/src/node/features/fs.ts +64 -25
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +25 -18
- package/src/node/features/ink.ts +13 -13
- package/src/node/features/ipc-socket.ts +8 -8
- package/src/node/features/networking.ts +3 -3
- package/src/node/features/os.ts +7 -7
- package/src/node/features/package-finder.ts +15 -15
- package/src/node/features/proc.ts +1 -1
- package/src/node/features/ui.ts +13 -13
- package/src/node/features/vm.ts +4 -4
- package/src/scaffolds/generated.ts +1 -1
- package/src/servers/express.ts +6 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/test/interceptor-chain.test.ts +61 -0
- package/docs/apis/features/node/window-manager.md +0 -445
- package/docs/examples/window-manager-layouts.md +0 -180
- package/docs/examples/window-manager.md +0 -125
- package/docs/window-manager-fix.md +0 -249
- package/scripts/test-window-manager-lifecycle.ts +0 -86
- package/scripts/test-window-manager.ts +0 -43
- package/src/node/features/window-manager.ts +0 -1603
|
@@ -125,6 +125,44 @@ export type ConversationState = z.infer<typeof ConversationStateSchema>
|
|
|
125
125
|
|
|
126
126
|
export type AskOptions = {
|
|
127
127
|
maxTokens?: number
|
|
128
|
+
/**
|
|
129
|
+
* When provided, enables OpenAI Structured Outputs. The model is constrained
|
|
130
|
+
* to return JSON matching this Zod schema. The return value of ask() will be
|
|
131
|
+
* the parsed object instead of a raw string.
|
|
132
|
+
*/
|
|
133
|
+
schema?: z.ZodType
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Recursively set `additionalProperties: false` on every object-type node
|
|
138
|
+
* in a JSON Schema tree. OpenAI strict mode requires this at every level.
|
|
139
|
+
* Also ensures every object has a `required` array listing all its property keys.
|
|
140
|
+
*/
|
|
141
|
+
function strictifySchema(schema: Record<string, any>): Record<string, any> {
|
|
142
|
+
const clone = { ...schema }
|
|
143
|
+
|
|
144
|
+
if (clone.type === 'object' && clone.properties) {
|
|
145
|
+
clone.additionalProperties = false
|
|
146
|
+
clone.required = Object.keys(clone.properties)
|
|
147
|
+
const props: Record<string, any> = {}
|
|
148
|
+
for (const [key, val] of Object.entries(clone.properties)) {
|
|
149
|
+
props[key] = strictifySchema(val as Record<string, any>)
|
|
150
|
+
}
|
|
151
|
+
clone.properties = props
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (clone.items) {
|
|
155
|
+
clone.items = strictifySchema(clone.items)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// anyOf / oneOf / allOf
|
|
159
|
+
for (const combiner of ['anyOf', 'oneOf', 'allOf'] as const) {
|
|
160
|
+
if (Array.isArray(clone[combiner])) {
|
|
161
|
+
clone[combiner] = clone[combiner].map((s: Record<string, any>) => strictifySchema(s))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return clone
|
|
128
166
|
}
|
|
129
167
|
|
|
130
168
|
/**
|
|
@@ -151,6 +189,16 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
151
189
|
|
|
152
190
|
static { Feature.register(this, 'conversation') }
|
|
153
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Pluggable tool executor. Called for each tool invocation with the tool
|
|
194
|
+
* name, parsed args, and the default handler. Return the serialized result string.
|
|
195
|
+
* The Assistant replaces this to wire in beforeToolCall/afterToolCall interceptors.
|
|
196
|
+
*/
|
|
197
|
+
toolExecutor: ((name: string, args: Record<string, any>, handler: (...args: any[]) => Promise<any>) => Promise<string>) | null = null
|
|
198
|
+
|
|
199
|
+
/** The active structured output schema for the current ask() call, if any. */
|
|
200
|
+
private _activeSchema: z.ZodType | null = null
|
|
201
|
+
|
|
154
202
|
/** Resolved max tokens: per-call override > options-level > undefined (no limit). */
|
|
155
203
|
private get maxTokens(): number | undefined {
|
|
156
204
|
return (this.state.get('callMaxTokens') as number | null) ?? this.options.maxTokens ?? undefined
|
|
@@ -419,6 +467,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
419
467
|
*/
|
|
420
468
|
async ask(content: string | ContentPart[], options?: AskOptions): Promise<string> {
|
|
421
469
|
this.state.set('callMaxTokens', options?.maxTokens ?? null)
|
|
470
|
+
this._activeSchema = options?.schema ?? null
|
|
422
471
|
|
|
423
472
|
// Auto-compact before adding the new message
|
|
424
473
|
if (this.options.autoCompact) {
|
|
@@ -436,6 +485,8 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
436
485
|
this.emit('userMessage', content)
|
|
437
486
|
|
|
438
487
|
try {
|
|
488
|
+
let raw: string
|
|
489
|
+
|
|
439
490
|
if (this.apiMode === 'responses') {
|
|
440
491
|
const previousResponseId = this.state.get('lastResponseId') || undefined
|
|
441
492
|
let input: OpenAI.Responses.ResponseInput
|
|
@@ -449,17 +500,31 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
449
500
|
input = this.messagesToResponsesInput()
|
|
450
501
|
}
|
|
451
502
|
|
|
452
|
-
|
|
503
|
+
raw = await this.runResponsesLoop({
|
|
453
504
|
turn: 1,
|
|
454
505
|
accumulated: '',
|
|
455
506
|
input,
|
|
456
507
|
previousResponseId,
|
|
457
508
|
})
|
|
509
|
+
} else {
|
|
510
|
+
raw = await this.runChatCompletionLoop({ turn: 1, accumulated: '' })
|
|
458
511
|
}
|
|
459
512
|
|
|
460
|
-
|
|
513
|
+
// When a structured output schema is active, parse the JSON response
|
|
514
|
+
if (this._activeSchema) {
|
|
515
|
+
try {
|
|
516
|
+
const parsed = JSON.parse(raw)
|
|
517
|
+
return parsed
|
|
518
|
+
} catch {
|
|
519
|
+
// Model returned something that isn't valid JSON — return raw
|
|
520
|
+
return raw
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return raw
|
|
461
525
|
} finally {
|
|
462
526
|
this.state.set('callMaxTokens', null)
|
|
527
|
+
this._activeSchema = null
|
|
463
528
|
}
|
|
464
529
|
}
|
|
465
530
|
|
|
@@ -545,6 +610,28 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
545
610
|
return input
|
|
546
611
|
}
|
|
547
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Build the OpenAI response_format / text.format config from the active Zod schema.
|
|
615
|
+
* Returns undefined when no schema is active.
|
|
616
|
+
*/
|
|
617
|
+
private get structuredOutputConfig(): { name: string; schema: Record<string, any>; strict: true } | undefined {
|
|
618
|
+
if (!this._activeSchema) return undefined
|
|
619
|
+
|
|
620
|
+
const raw = (this._activeSchema as any).toJSONSchema() as Record<string, any>
|
|
621
|
+
const strict = strictifySchema(raw)
|
|
622
|
+
|
|
623
|
+
// Derive a name from the schema description or fall back to a default.
|
|
624
|
+
// OpenAI requires [a-zA-Z0-9_-] max 64 chars.
|
|
625
|
+
const desc = raw.description || 'structured_output'
|
|
626
|
+
const name = desc.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
name,
|
|
630
|
+
schema: { type: strict.type || 'object', properties: strict.properties, required: strict.required, additionalProperties: false },
|
|
631
|
+
strict: true,
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
548
635
|
/** Returns the OpenAI client instance from the container. */
|
|
549
636
|
get openai() {
|
|
550
637
|
let baseURL = this.options.clientOptions?.baseURL ? this.options.clientOptions.baseURL : undefined
|
|
@@ -603,6 +690,40 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
603
690
|
})
|
|
604
691
|
}
|
|
605
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Execute a single tool call, routing through the pluggable toolExecutor
|
|
695
|
+
* if one is set (e.g. by the Assistant's interceptor chain).
|
|
696
|
+
*/
|
|
697
|
+
private async executeTool(toolName: string, rawArgs: string): Promise<string> {
|
|
698
|
+
const tool = this.tools[toolName]
|
|
699
|
+
const callCount = (this.state.get('toolCalls') || 0) + 1
|
|
700
|
+
this.state.set('toolCalls', callCount)
|
|
701
|
+
|
|
702
|
+
if (!tool) {
|
|
703
|
+
const result = JSON.stringify({ error: `Unknown tool: ${toolName}` })
|
|
704
|
+
this.emit('toolError', toolName, result)
|
|
705
|
+
return result
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (this.toolExecutor) {
|
|
709
|
+
const args = rawArgs ? JSON.parse(rawArgs) : {}
|
|
710
|
+
return this.toolExecutor(toolName, args, tool.handler)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
const args = rawArgs ? JSON.parse(rawArgs) : {}
|
|
715
|
+
this.emit('toolCall', toolName, args)
|
|
716
|
+
const output = await tool.handler(args)
|
|
717
|
+
const result = typeof output === 'string' ? output : JSON.stringify(output)
|
|
718
|
+
this.emit('toolResult', toolName, result)
|
|
719
|
+
return result
|
|
720
|
+
} catch (err: any) {
|
|
721
|
+
const result = JSON.stringify({ error: err.message || String(err) })
|
|
722
|
+
this.emit('toolError', toolName, err)
|
|
723
|
+
return result
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
606
727
|
/**
|
|
607
728
|
* Runs the streaming Responses API loop. Handles local function calls by
|
|
608
729
|
* executing handlers and submitting `function_call_output` items until
|
|
@@ -625,6 +746,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
625
746
|
this.state.set('streaming', true)
|
|
626
747
|
this.emit('turnStart', { turn, isFollowUp: turn > 1 })
|
|
627
748
|
|
|
749
|
+
const textFormat = this.structuredOutputConfig
|
|
750
|
+
? { text: { format: { type: 'json_schema' as const, ...this.structuredOutputConfig } } }
|
|
751
|
+
: {}
|
|
752
|
+
|
|
628
753
|
try {
|
|
629
754
|
const stream = await this.openai.raw.responses.create({
|
|
630
755
|
model: this.model as OpenAI.Responses.ResponseCreateParams['model'],
|
|
@@ -634,6 +759,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
634
759
|
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto', parallel_tool_calls: true } : {}),
|
|
635
760
|
...(this.responsesInstructions ? { instructions: this.responsesInstructions } : {}),
|
|
636
761
|
...(this.maxTokens ? { max_output_tokens: this.maxTokens } : {}),
|
|
762
|
+
...textFormat,
|
|
637
763
|
})
|
|
638
764
|
|
|
639
765
|
for await (const event of stream) {
|
|
@@ -690,27 +816,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
690
816
|
|
|
691
817
|
const functionOutputs: OpenAI.Responses.ResponseInputItem.FunctionCallOutput[] = []
|
|
692
818
|
for (const call of functionCalls) {
|
|
693
|
-
const
|
|
694
|
-
const tool = this.tools[toolName]
|
|
695
|
-
const callCount = (this.state.get('toolCalls') || 0) + 1
|
|
696
|
-
this.state.set('toolCalls', callCount)
|
|
697
|
-
|
|
698
|
-
let result: string
|
|
699
|
-
if (!tool) {
|
|
700
|
-
result = JSON.stringify({ error: `Unknown tool: ${toolName}` })
|
|
701
|
-
this.emit('toolError', toolName, result)
|
|
702
|
-
} else {
|
|
703
|
-
try {
|
|
704
|
-
const args = call.arguments ? JSON.parse(call.arguments) : {}
|
|
705
|
-
this.emit('toolCall', toolName, args)
|
|
706
|
-
const output = await tool.handler(args)
|
|
707
|
-
result = typeof output === 'string' ? output : JSON.stringify(output)
|
|
708
|
-
this.emit('toolResult', toolName, result)
|
|
709
|
-
} catch (err: any) {
|
|
710
|
-
result = JSON.stringify({ error: err.message || String(err) })
|
|
711
|
-
this.emit('toolError', toolName, err)
|
|
712
|
-
}
|
|
713
|
-
}
|
|
819
|
+
const result = await this.executeTool(call.name, call.arguments || '{}')
|
|
714
820
|
|
|
715
821
|
this.pushMessage({
|
|
716
822
|
role: 'tool',
|
|
@@ -785,6 +891,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
785
891
|
let turnContent = ''
|
|
786
892
|
let toolCalls: Array<{ id: string; function: { name: string; arguments: string }; type: 'function' }> = []
|
|
787
893
|
|
|
894
|
+
const responseFormat = this.structuredOutputConfig
|
|
895
|
+
? { response_format: { type: 'json_schema' as const, json_schema: this.structuredOutputConfig } }
|
|
896
|
+
: {}
|
|
897
|
+
|
|
788
898
|
try {
|
|
789
899
|
const stream = await this.openai.raw.chat.completions.create({
|
|
790
900
|
model: this.model,
|
|
@@ -792,6 +902,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
792
902
|
stream: true,
|
|
793
903
|
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
|
|
794
904
|
...(this.maxTokens ? { max_tokens: this.maxTokens } : {}),
|
|
905
|
+
...responseFormat,
|
|
795
906
|
})
|
|
796
907
|
|
|
797
908
|
for await (const chunk of stream) {
|
|
@@ -850,28 +961,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
850
961
|
this.emit('toolCallsStart', toolCalls)
|
|
851
962
|
|
|
852
963
|
for (const tc of toolCalls) {
|
|
853
|
-
const
|
|
854
|
-
const tool = this.tools[toolName]
|
|
855
|
-
const callCount = (this.state.get('toolCalls') || 0) + 1
|
|
856
|
-
this.state.set('toolCalls', callCount)
|
|
857
|
-
|
|
858
|
-
let result: string
|
|
859
|
-
|
|
860
|
-
if (!tool) {
|
|
861
|
-
result = JSON.stringify({ error: `Unknown tool: ${toolName}` })
|
|
862
|
-
this.emit('toolError', toolName, result)
|
|
863
|
-
} else {
|
|
864
|
-
try {
|
|
865
|
-
const args = JSON.parse(tc.function.arguments)
|
|
866
|
-
this.emit('toolCall', toolName, args)
|
|
867
|
-
const output = await tool.handler(args)
|
|
868
|
-
result = typeof output === 'string' ? output : JSON.stringify(output)
|
|
869
|
-
this.emit('toolResult', toolName, result)
|
|
870
|
-
} catch (err: any) {
|
|
871
|
-
result = JSON.stringify({ error: err.message || String(err) })
|
|
872
|
-
this.emit('toolError', toolName, err)
|
|
873
|
-
}
|
|
874
|
-
}
|
|
964
|
+
const result = await this.executeTool(tc.function.name, tc.function.arguments)
|
|
875
965
|
|
|
876
966
|
const toolMessage: OpenAI.Chat.Completions.ChatCompletionToolMessageParam = {
|
|
877
967
|
role: 'tool',
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A composable middleware chain. Each interceptor receives a mutable
|
|
3
|
+
* context and a `next` function. Calling `next()` continues the chain;
|
|
4
|
+
* skipping it short-circuits.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type InterceptorFn<T> = (ctx: T, next: () => Promise<void>) => Promise<void>
|
|
8
|
+
|
|
9
|
+
export class InterceptorChain<T> {
|
|
10
|
+
private fns: InterceptorFn<T>[] = []
|
|
11
|
+
|
|
12
|
+
add(fn: InterceptorFn<T>): void {
|
|
13
|
+
this.fns.push(fn)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
remove(fn: InterceptorFn<T>): void {
|
|
17
|
+
const idx = this.fns.indexOf(fn)
|
|
18
|
+
if (idx !== -1) this.fns.splice(idx, 1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get hasInterceptors(): boolean {
|
|
22
|
+
return this.fns.length > 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get size(): number {
|
|
26
|
+
return this.fns.length
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async run(ctx: T, final: () => Promise<void>): Promise<void> {
|
|
30
|
+
let index = 0
|
|
31
|
+
const fns = this.fns
|
|
32
|
+
|
|
33
|
+
const next = async (): Promise<void> => {
|
|
34
|
+
if (index < fns.length) {
|
|
35
|
+
const fn = fns[index++]!
|
|
36
|
+
await fn(ctx, next)
|
|
37
|
+
} else {
|
|
38
|
+
await final()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await next()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BeforeAskCtx {
|
|
47
|
+
question: string | any[]
|
|
48
|
+
options?: any
|
|
49
|
+
result?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ToolCallCtx {
|
|
53
|
+
name: string
|
|
54
|
+
args: Record<string, any>
|
|
55
|
+
result?: string
|
|
56
|
+
error?: any
|
|
57
|
+
skip?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BeforeResponseCtx {
|
|
61
|
+
text: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BeforeTurnCtx {
|
|
65
|
+
turn: number
|
|
66
|
+
isFollowUp: boolean
|
|
67
|
+
messages: any[]
|
|
68
|
+
skip?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface InterceptorPoints {
|
|
72
|
+
beforeAsk: BeforeAskCtx
|
|
73
|
+
beforeTurn: BeforeTurnCtx
|
|
74
|
+
beforeToolCall: ToolCallCtx
|
|
75
|
+
afterToolCall: ToolCallCtx
|
|
76
|
+
beforeResponse: BeforeResponseCtx
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type InterceptorPoint = keyof InterceptorPoints
|