Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions vector/apps/web/app/api/assets/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function GET(req: Request) {
const tags = parseTags(searchParams)
const logQuery = query.replace(/[\r\n\t]+/g, ' ').trim().slice(0, 80)
const logTags = tags.map((t) => t.slice(0, 24)).join('|')
const limit = Math.max(1, Math.min(50, Number(searchParams.get('limit') || '8')))
const limit = Math.max(1, Math.min(60, Number(searchParams.get('limit') || '8')))
const override = process.env.CATALOG_API_URL?.trim()
const provider = !override || override.toLowerCase() === 'roblox' ? 'roblox' : 'proxy'
const t0 = Date.now()
Expand All @@ -48,4 +48,3 @@ export async function GET(req: Request) {
})
}
}

23 changes: 21 additions & 2 deletions vector/apps/web/app/api/proposals/[id]/apply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export async function POST(req: Request, ctx: { params: { id: string } }) {
const opKind = typeof (body as any)?.op === 'string' ? String((body as any).op) : undefined
const failed = (body as any)?.ok === false
if (wf && isAsset && failed) {
const fallback = 'CATALOG_UNAVAILABLE Asset search/insert failed. Create the requested objects manually using create_instance/set_properties or Luau edits.'
try { pushChunk(wf, 'fallback.asset manual_required') } catch (e) { console.error('Failed to push chunk for asset fallback', e) }
const fallback = 'CATALOG_UNAVAILABLE Asset search/insert failed. Consider manual geometry or alternative assets.'
try { pushChunk(wf, 'fallback.asset manual_suggest') } catch (e) { console.error('Failed to push chunk for asset fallback', e) }
updateTaskState(wf, (state) => {
state.history.push({ role: 'system', content: fallback + (opKind ? ` op=${opKind}` : ''), at: Date.now() })
})
Expand All @@ -66,6 +66,25 @@ export async function POST(req: Request, ctx: { params: { id: string } }) {
)
}
}
// Log asset operations verbosely for terminal diagnostics
if (typeof (body as any)?.type === 'string' && (body as any).type === 'asset_op') {
const opKind = typeof (body as any)?.op === 'string' ? String((body as any).op) : 'unknown'
const ok = (body as any)?.ok === true
const assetId = (body as any)?.assetId
const parentPath = typeof (body as any)?.parentPath === 'string' ? String((body as any)?.parentPath) : undefined
const insertedPath = typeof (body as any)?.insertedPath === 'string' ? String((body as any)?.insertedPath) : undefined
const query = typeof (body as any)?.query === 'string' ? String((body as any)?.query) : undefined
const error = typeof (body as any)?.error === 'string' ? String((body as any)?.error) : undefined
const base = `[proposals.apply] asset_op op=${opKind}`
+ (assetId != null ? ` id=${assetId}` : '')
+ (parentPath ? ` parent=${parentPath}` : '')
+ (query ? ` query="${query}"` : '')
if (ok) {
console.log(`${base} ok inserted=${insertedPath || 'n/a'}`)
} else {
console.warn(`${base} failed error=${error || 'unknown'}`)
}
}
console.log(
`[proposals.apply] id=${id} ok=${!!after} workflowId=${after?.workflowId || 'n/a'} payloadKeys=${Object.keys(body || {}).length}`,
)
Expand Down
42 changes: 36 additions & 6 deletions vector/apps/web/lib/catalog/search.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
export type CatalogItem = { id: number; name: string; creator: string; type: string; thumbnailUrl?: string }
export type CatalogItem = {
id: number
name: string
creator: string
type: string
thumbnailUrl?: string
creatorId?: number
creatorType?: string
isVerified?: boolean
}

type CatalogSearchOptions = {
tags?: string[]
}

const ROBLOX_CATALOG_URL = 'https://catalog.roblox.com/v1/search/items/details'
const ROBLOX_THUMBNAIL_URL = 'https://thumbnails.roblox.com/v1/assets'
const ROBLOX_ALLOWED_LIMITS = [10, 28, 30] as const
const ROBLOX_ALLOWED_LIMITS = [10, 28, 30, 60] as const

const TAG_CATEGORY_MAP: Record<string, 'Models' | 'Audio' | 'Decals' | 'Animations'> = {
audio: 'Audio',
Expand Down Expand Up @@ -52,7 +61,7 @@ function normalizeString(value?: string | null): string | undefined {
}

function pickRobloxLimit(limit: number): number {
const safe = Math.max(1, Math.min(50, Number.isFinite(limit) ? limit : 8))
const safe = Math.max(1, Math.min(60, Number.isFinite(limit) ? limit : 8))
for (const allowed of ROBLOX_ALLOWED_LIMITS) {
if (safe <= allowed) return allowed
}
Expand Down Expand Up @@ -98,7 +107,7 @@ async function fetchRobloxThumbnails(ids: number[]): Promise<Map<number, string>

async function fetchFromRoblox(query: string, limit: number, opts?: CatalogSearchOptions): Promise<CatalogItem[]> {
const trimmedQuery = query?.trim() ?? ''
const desiredLimit = Math.max(1, Math.min(50, Number.isFinite(limit) ? limit : 8))
const desiredLimit = Math.max(1, Math.min(60, Number.isFinite(limit) ? limit : 8))
const requestLimit = pickRobloxLimit(desiredLimit)
const category = deriveCategory(opts?.tags)
const params = new URLSearchParams({
Expand All @@ -109,6 +118,7 @@ async function fetchFromRoblox(query: string, limit: number, opts?: CatalogSearc
SortType: '3',
})
// Optional: restrict to free assets only when explicitly requested
// Default free-only OFF to broaden results; env can enable explicitly
const freeOnly = String(process.env.CATALOG_FREE_ONLY || '0') === '1'
if (freeOnly) {
// Prefer Roblox sales filter for free items; explicit price filters often 0-out results
Expand Down Expand Up @@ -156,12 +166,19 @@ async function fetchFromRoblox(query: string, limit: number, opts?: CatalogSearc
for (const entry of sliced) {
const id = Number(entry?.id)
if (!Number.isFinite(id)) continue
const creatorIdRaw = entry?.creatorTargetId ?? entry?.creatorId
const creatorId = typeof creatorIdRaw === 'number' ? creatorIdRaw : Number(creatorIdRaw)
const creatorType = typeof entry?.creatorType === 'string' ? entry.creatorType : undefined
const isVerified = Boolean(entry?.creatorHasVerifiedBadge ?? entry?.hasVerifiedBadge)
items.push({
id,
name: typeof entry?.name === 'string' ? entry.name : `Asset ${id}`,
creator: typeof entry?.creatorName === 'string' ? entry.creatorName : 'Unknown',
type: assetTypeLabel(entry?.assetType),
thumbnailUrl: thumbMap.get(id),
creatorId: Number.isFinite(creatorId) ? Number(creatorId) : undefined,
creatorType,
isVerified,
})
}
return items
Expand Down Expand Up @@ -207,7 +224,7 @@ async function fetchFromCreatorStoreToolbox(query: string, limit: number, opts?:
// Broaden results by allowing all creators (verified and non-verified)
url.searchParams.set('includeOnlyVerifiedCreators', 'false')
// Free-only via price caps in cents
if (String(process.env.CATALOG_FREE_ONLY || '0') === '1') {
if (String(process.env.CATALOG_FREE_ONLY || '1') === '1') {
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for CATALOG_FREE_ONLY has changed from '0' to '1', but this differs from the Roblox catalog implementation on line 122 which still uses '0'. This inconsistency could lead to different filtering behavior between catalog sources.

Suggested change
if (String(process.env.CATALOG_FREE_ONLY || '1') === '1') {
if (String(process.env.CATALOG_FREE_ONLY || '0') === '1') {

Copilot uses AI. Check for mistakes.
url.searchParams.set('minPriceCents', '0')
url.searchParams.set('maxPriceCents', '0')
}
Expand Down Expand Up @@ -244,7 +261,20 @@ async function fetchFromCreatorStoreToolbox(query: string, limit: number, opts?:
// Toolbox imagePreviewAssets are asset ids; thumbnails route might be needed separately.
// Use undefined here; downstream can fetch thumbnails by id if needed.
}
items.push({ id, name, creator, type, thumbnailUrl })
const creatorIdRaw = entry?.creator?.id ?? entry?.creatorId
const creatorId = typeof creatorIdRaw === 'number' ? creatorIdRaw : Number(creatorIdRaw)
const creatorType = typeof entry?.creator?.type === 'string' ? entry.creator.type : undefined
const isVerified = Boolean(entry?.creator?.hasVerifiedBadge ?? entry?.creatorHasVerifiedBadge)
items.push({
id,
name,
creator,
type,
thumbnailUrl,
creatorId: Number.isFinite(creatorId) ? Number(creatorId) : undefined,
creatorType,
isVerified,
})
}
return items.slice(0, Math.max(1, Math.min(50, limit || 8)))
} finally {
Expand Down
4 changes: 4 additions & 0 deletions vector/apps/web/lib/orchestrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ Coordinates multi-step tasks, provider execution, prompt assembly, and proposal
- `taskState.ts` – State tracking for tasks.
- `proposals.ts` / `autoApprove.ts` – Proposal generation and auto-approval logic.

## Behaviour Highlights
- Catalog fallback switches the task into **manual mode**. Once enabled, asset search/insert tools are blocked until manual geometry (Parts + Luau) is produced or the user explicitly re-enables catalog usage.
- When a plan already exists, additional `<start_plan>` calls must reuse the same steps; changes require `<update_plan>`.

## Extending Providers
Add a new file under `providers/` exporting a standardized interface (see existing providers for shape).
Loading