@soederpop/luca 0.0.14 → 0.0.16

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.
@@ -0,0 +1,3 @@
1
+ # Luca Framework Assistant
2
+
3
+ Your job is to help people use the Luca framework
@@ -0,0 +1,3 @@
1
+ export function started() {
2
+ console.log('Assistant started!')
3
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod'
2
+
3
+ export const schemas = {
4
+ README: z.object({}).describe('CALL THIS README FUNCTION AS EARLY AS POSSIBLE')
5
+ }
6
+
7
+ export function README(options: z.infer<typeof schemas.README>) {
8
+ return 'YO YO'
9
+ }
10
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soederpop/luca",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "website": "https://luca.soederpop.com",
5
5
  "description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
6
6
  "author": "jon soeder aka the people's champ <jon@soederpop.com>",
@@ -646,6 +646,11 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
646
646
  this.conversation.state.set('thread', threadId)
647
647
  this.conversation.state.set('messages', messages)
648
648
  this.state.set('conversationId', existing.id)
649
+
650
+ // Restore lastResponseId so the Responses API can continue the chain
651
+ if (existing.metadata?.lastResponseId) {
652
+ this.conversation.state.set('lastResponseId', existing.metadata.lastResponseId)
653
+ }
649
654
  } else {
650
655
  // Fresh conversation — just set thread
651
656
  this.conversation.state.set('thread', threadId)
@@ -407,11 +407,23 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
407
407
 
408
408
  try {
409
409
  if (this.apiMode === 'responses') {
410
+ const previousResponseId = this.state.get('lastResponseId') || undefined
411
+ let input: OpenAI.Responses.ResponseInput
412
+
413
+ if (previousResponseId) {
414
+ // Can chain via previous_response_id — only send the new user message
415
+ input = [this.toResponsesUserMessage(content)]
416
+ } else {
417
+ // No previous response ID (first call or resumed from disk).
418
+ // Convert full message history to Responses API input so the model has context.
419
+ input = this.messagesToResponsesInput()
420
+ }
421
+
410
422
  return await this.runResponsesLoop({
411
423
  turn: 1,
412
424
  accumulated: '',
413
- input: [this.toResponsesUserMessage(content)],
414
- previousResponseId: this.state.get('lastResponseId') || undefined,
425
+ input,
426
+ previousResponseId,
415
427
  })
416
428
  }
417
429
 
@@ -456,6 +468,53 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
456
468
  }
457
469
  }
458
470
 
471
+ /**
472
+ * Convert the full Chat Completions message history into Responses API input items.
473
+ * Used when resuming a conversation without a previous_response_id.
474
+ */
475
+ private messagesToResponsesInput(): OpenAI.Responses.ResponseInput {
476
+ const input: OpenAI.Responses.ResponseInput = []
477
+
478
+ for (const msg of this.messages) {
479
+ if (msg.role === 'system' || msg.role === 'developer') {
480
+ // System/developer messages are handled via the instructions parameter
481
+ continue
482
+ }
483
+
484
+ if (msg.role === 'user') {
485
+ if (typeof msg.content === 'string') {
486
+ input.push({
487
+ type: 'message',
488
+ role: 'user',
489
+ content: [{ type: 'input_text', text: msg.content }],
490
+ })
491
+ } else if (Array.isArray(msg.content)) {
492
+ input.push(this.toResponsesUserMessage(msg.content as ContentPart[]))
493
+ }
494
+ continue
495
+ }
496
+
497
+ if (msg.role === 'assistant') {
498
+ const content = typeof msg.content === 'string' ? msg.content : (msg.content || []).map((p: any) => p.text || '').join('')
499
+ if (content) {
500
+ input.push({
501
+ type: 'message',
502
+ role: 'assistant',
503
+ content: [{ type: 'output_text', text: content, annotations: [] }],
504
+ id: `msg_replay-${input.length}`,
505
+ status: 'completed',
506
+ } as any)
507
+ }
508
+ continue
509
+ }
510
+
511
+ // Tool results — skip in the replay since the assistant's tool_calls won't have matching IDs
512
+ // The model will still understand context from the assistant messages that followed
513
+ }
514
+
515
+ return input
516
+ }
517
+
459
518
  /** Returns the OpenAI client instance from the container. */
460
519
  get openai() {
461
520
  let baseURL = this.options.clientOptions?.baseURL ? this.options.clientOptions.baseURL : undefined
@@ -488,13 +547,17 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
488
547
  const id = this.state.get('id')!
489
548
  const existing = await this.history.load(id)
490
549
 
550
+ // Persist lastResponseId so the Responses API can continue the chain on resume
551
+ const lastResponseId = this.state.get('lastResponseId')
552
+ const responseMeta = lastResponseId ? { lastResponseId } : {}
553
+
491
554
  if (existing) {
492
555
  existing.messages = this.messages
493
556
  existing.model = this.model
494
557
  if (opts?.title) existing.title = opts.title
495
558
  if (opts?.tags) existing.tags = opts.tags
496
559
  if (opts?.thread) existing.thread = opts.thread
497
- if (opts?.metadata) existing.metadata = { ...existing.metadata, ...opts.metadata }
560
+ existing.metadata = { ...existing.metadata, ...responseMeta, ...(opts?.metadata || {}) }
498
561
  await this.history.save(existing)
499
562
  return existing
500
563
  }
@@ -506,7 +569,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
506
569
  messages: this.messages,
507
570
  tags: opts?.tags || this.options.tags || [],
508
571
  thread: opts?.thread || this.options.thread || this.state.get('thread'),
509
- metadata: opts?.metadata || this.options.metadata || {},
572
+ metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
510
573
  })
511
574
  }
512
575
 
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-20T03:57:57.385Z
2
+ // Generated at: 2026-03-20T16:22:30.732Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -17,6 +17,7 @@ export const argsSchema = CommandOptionsSchema.extend({
17
17
  list: z.boolean().optional().describe('List recent conversations and exit'),
18
18
  historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Override history persistence mode'),
19
19
  offRecord: z.boolean().optional().describe('Alias for --history-mode lifecycle (ephemeral, no persistence)'),
20
+ clear: z.boolean().optional().describe('Clear the conversation history for the resolved history mode and exit'),
20
21
  })
21
22
 
22
23
  export default async function chat(options: z.infer<typeof argsSchema>, context: ContainerContext) {
@@ -72,6 +73,17 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
72
73
 
73
74
  const assistant = manager.create(name, createOptions)
74
75
 
76
+ // --clear: wipe history for the current mode and exit
77
+ if (options.clear) {
78
+ const deleted = await assistant.clearHistory()
79
+ if (deleted > 0) {
80
+ console.log(ui.colors.green(` Cleared ${deleted} conversation(s) for ${ui.colors.cyan(name)} (${historyMode} mode).`))
81
+ } else {
82
+ console.log(ui.colors.dim(` No history to clear for ${ui.colors.cyan(name)} (${historyMode} mode).`))
83
+ }
84
+ return
85
+ }
86
+
75
87
  // --list: show recent conversations and exit
76
88
  if (options.list) {
77
89
  const history = await assistant.listHistory({ limit: 20 })