|
| 1 | +/** |
| 2 | + * AssetPacksSynthesis — formal pipeline execution (V48 Gate 3). |
| 3 | + * |
| 4 | + * Runs the synthesis measurement on the real Bitcode primitives instead of a |
| 5 | + * hand-rolled inference loop: |
| 6 | + * PipelineExecution → phase → factoryAgent (AgentExecution) → step |
| 7 | + * → createFailsafeGenerationSequence (Failsafe[prepare|chunk|stitch] ∘ |
| 8 | + * Thricified[reason|judge|structured_output]) |
| 9 | + * |
| 10 | + * Every LLM call's prompt is built UPWARDS through the execution's prompt |
| 11 | + * registry: layered PromptParts registered on the AgentExecution.prompt |
| 12 | + * (pipeline identity + source-safety, phase lens role, agent measurement |
| 13 | + * catalog + rules, step candidate shape) compose via buildHierarchicalPrompt. |
| 14 | + * The exclusion-filtered source inventory is produced by a real ExecutionTool. |
| 15 | + * |
| 16 | + * Source-safety: the formal LLM substeps store full prompt/response content, |
| 17 | + * which is withheld universally by the streaming-layer filter |
| 18 | + * (sourceSafeStreamEvent). assertSourceSafeCandidates here is the local |
| 19 | + * defense-in-depth check that no admitted candidate leaks raw source. |
| 20 | + */ |
| 21 | + |
| 22 | +import { z } from 'zod'; |
| 23 | +import type { Execution } from '@bitcode/execution-generics/Execution'; |
| 24 | +import { PipelineExecution } from '@bitcode/pipelines-generics'; |
| 25 | +import { |
| 26 | + factoryAgent, |
| 27 | + createFailsafeGenerationSequence, |
| 28 | + ExecutionTool, |
| 29 | + type AgentStep, |
| 30 | +} from '@bitcode/agent-generics'; |
| 31 | +import { resolveDefaultLLMConfig } from '@bitcode/generic-llms'; |
| 32 | +import type { PromptPart } from '@bitcode/prompts/parts/PromptPart'; |
| 33 | + |
| 34 | +import { |
| 35 | + applyExclusionsToInventory, |
| 36 | + measurementCatalogForLens, |
| 37 | + type AssetPackMeasurementSpec, |
| 38 | + type AssetPacksSynthesisLens, |
| 39 | + type AssetPacksSynthesisSourceInventory, |
| 40 | + type AssetPacksSynthesisSourceSample, |
| 41 | + type AssetPacksSynthesisSteering, |
| 42 | +} from './asset-packs-synthesis'; |
| 43 | + |
| 44 | +const part = (content: string): PromptPart => content as PromptPart; |
| 45 | + |
| 46 | +// ---- Layered system PromptParts (composed by buildHierarchicalPrompt) ------- |
| 47 | + |
| 48 | +const PIPELINE_IDENTITY = part( |
| 49 | + 'You are AssetPacksSynthesis, the single Bitcode synthesis and measurement pipeline. ' + |
| 50 | + 'Depositing and Reading are the same operation at the core: measuring source knowledge ' + |
| 51 | + 'into commercially legible AssetPack candidates; the lens carries the variance.', |
| 52 | +); |
| 53 | + |
| 54 | +const PIPELINE_SOURCE_SAFETY = part( |
| 55 | + 'Source-safety law (mandatory): never quote source code, secrets, or file contents in any ' + |
| 56 | + 'title, summary, or rationale. Describe knowledge and capability, never raw text. Honor the ' + |
| 57 | + 'protected-IP exclusions absolutely — never cover, reference, or describe excluded material.', |
| 58 | +); |
| 59 | + |
| 60 | +const LENS_ROLE: Record<AssetPacksSynthesisLens, PromptPart> = { |
| 61 | + deposit: part( |
| 62 | + 'Lens: deposit. Depositors supply AssetPacks (bounded, source-safe slices of repository ' + |
| 63 | + 'knowledge) into a Depository where future buyers find Need-fitting packs. Synthesize ' + |
| 64 | + 'distinct deposit AssetPack options the depositor can review and admit.', |
| 65 | + ), |
| 66 | + read: part( |
| 67 | + 'Lens: read. Readers ask Bitcode to satisfy a reviewed Need by finding and synthesizing ' + |
| 68 | + 'Need-fitting AssetPacks from Depository source. Synthesize distinct Need-fitting ' + |
| 69 | + 'AssetPack candidates the reader can review and buy.', |
| 70 | + ), |
| 71 | +}; |
| 72 | + |
| 73 | +function buildCatalogPart(catalog: AssetPackMeasurementSpec[]): PromptPart { |
| 74 | + return part( |
| 75 | + [ |
| 76 | + 'measurements is an object with EXACTLY these keys, each an honest 0..1 volume:', |
| 77 | + ...catalog.map((spec) => ` ${spec.measurementKind}: ${spec.guidance}`), |
| 78 | + 'Justify every measurement in measurementRationale.', |
| 79 | + ].join('\n'), |
| 80 | + ); |
| 81 | +} |
| 82 | + |
| 83 | +function buildRulesPart(candidateKinds: string[], maxCandidates: number): PromptPart { |
| 84 | + return part( |
| 85 | + [ |
| 86 | + `Synthesize 2-${maxCandidates} DISTINCT candidates from the repository inventory.`, |
| 87 | + '- coveredSourcePaths must be chosen ONLY from the provided inventory paths, exactly as written.', |
| 88 | + '- Each candidate is a distinct knowledge slice (different kind and coverage), commercially legible to a buyer.', |
| 89 | + `- candidate kind must be one of: ${candidateKinds.join(', ')}.`, |
| 90 | + ].join('\n'), |
| 91 | + ); |
| 92 | +} |
| 93 | + |
| 94 | +function buildShapePart(catalog: AssetPackMeasurementSpec[], maxCandidates: number): PromptPart { |
| 95 | + return part( |
| 96 | + [ |
| 97 | + 'Return ONLY a JSON object with this EXACT top-level shape (no markdown, no prose, no other top-level key):', |
| 98 | + `{"options":[{"kind":string,"title":string,"summary":string,"coveredSourcePaths":[string],"measurements":{${catalog |
| 99 | + .map((spec) => `"${spec.measurementKind}":number`) |
| 100 | + .join(',')}},"measurementRationale":string,"confidence":number}]}`, |
| 101 | + `The top-level key MUST be "options" — an array of 2-${maxCandidates} candidate objects. Never return a bare array or any other wrapper key.`, |
| 102 | + ].join('\n'), |
| 103 | + ); |
| 104 | +} |
| 105 | + |
| 106 | +// ---- Formal source-inventory Tool ------------------------------------------ |
| 107 | + |
| 108 | +type InventoryToolArgs = { |
| 109 | + paths: string[]; |
| 110 | + samples: AssetPacksSynthesisSourceSample[]; |
| 111 | + exclusions: string[]; |
| 112 | +}; |
| 113 | + |
| 114 | +export class AssetPackInventoryTool extends ExecutionTool< |
| 115 | + (args: InventoryToolArgs) => Promise<AssetPacksSynthesisSourceInventory> |
| 116 | +> { |
| 117 | + use = async (args: InventoryToolArgs): Promise<AssetPacksSynthesisSourceInventory> => { |
| 118 | + return applyExclusionsToInventory( |
| 119 | + { paths: args.paths, samples: args.samples }, |
| 120 | + args.exclusions, |
| 121 | + ); |
| 122 | + }; |
| 123 | +} |
| 124 | + |
| 125 | +// ---- The formal synthesis run ---------------------------------------------- |
| 126 | + |
| 127 | +export interface FormalSynthesisRequest { |
| 128 | + lens: AssetPacksSynthesisLens; |
| 129 | + repositoryFullName: string; |
| 130 | + sourceBranch: string | null; |
| 131 | + sourceCommit: string | null; |
| 132 | + steering: AssetPacksSynthesisSteering; |
| 133 | + inventory: AssetPacksSynthesisSourceInventory; |
| 134 | + candidateKinds: string[]; |
| 135 | + maxCandidates: number; |
| 136 | +} |
| 137 | + |
| 138 | +export interface FormalSynthesisRawOption { |
| 139 | + kind: string; |
| 140 | + title: string; |
| 141 | + summary: string; |
| 142 | + coveredSourcePaths: string[]; |
| 143 | + measurements: Record<string, number>; |
| 144 | + measurementRationale: string; |
| 145 | + confidence: number; |
| 146 | +} |
| 147 | + |
| 148 | +export interface FormalSynthesisOutcome { |
| 149 | + options: FormalSynthesisRawOption[]; |
| 150 | + provider: string | null; |
| 151 | + model: string | null; |
| 152 | + totalTokens: number | null; |
| 153 | +} |
| 154 | + |
| 155 | +function sumLlmTokens(execution: Execution): number | null { |
| 156 | + let total = 0; |
| 157 | + let seen = false; |
| 158 | + const walk = (node: Execution) => { |
| 159 | + try { |
| 160 | + const usage = node.get<Record<string, number>>('llm', 'usage'); |
| 161 | + if (usage && typeof usage === 'object') { |
| 162 | + const prompt = |
| 163 | + usage.promptTokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.input_tokens ?? 0; |
| 164 | + const completion = |
| 165 | + usage.completionTokens ?? |
| 166 | + usage.completion_tokens ?? |
| 167 | + usage.outputTokens ?? |
| 168 | + usage.output_tokens ?? |
| 169 | + 0; |
| 170 | + const t = |
| 171 | + typeof usage.totalTokens === 'number' |
| 172 | + ? usage.totalTokens |
| 173 | + : typeof usage.total_tokens === 'number' |
| 174 | + ? usage.total_tokens |
| 175 | + : Number(prompt) + Number(completion); |
| 176 | + if (Number.isFinite(t) && t > 0) { |
| 177 | + total += t; |
| 178 | + seen = true; |
| 179 | + } |
| 180 | + } |
| 181 | + } catch {} |
| 182 | + for (const child of node.children.values()) walk(child); |
| 183 | + }; |
| 184 | + walk(execution); |
| 185 | + return seen ? total : null; |
| 186 | +} |
| 187 | + |
| 188 | +export async function synthesizeAssetPackCandidatesFormal( |
| 189 | + request: FormalSynthesisRequest, |
| 190 | + candidateSetSchema: z.ZodType<{ options: FormalSynthesisRawOption[] }>, |
| 191 | + execution: Execution, |
| 192 | +): Promise<FormalSynthesisOutcome> { |
| 193 | + const catalog = measurementCatalogForLens(request.lens); |
| 194 | + |
| 195 | + // Execution spine: Pipeline → Phase → Agent → Step → Generation (real nodes). |
| 196 | + const pipelineExec = new PipelineExecution( |
| 197 | + 'pipeline:asset-packs-synthesis', |
| 198 | + execution, |
| 199 | + { |
| 200 | + pipelineName: 'asset-packs-synthesis', |
| 201 | + family: 'asset_pack', |
| 202 | + posture: 'live', |
| 203 | + admittedSurface: request.lens, |
| 204 | + } as any, |
| 205 | + ); |
| 206 | + const phaseExec = pipelineExec.child(`phase:${request.lens}`); |
| 207 | + |
| 208 | + // A custom orchestration step (register layered prompts + run the formal |
| 209 | + // Failsafe∘Thricified generation). Typed as a plain Executor; factoryAgent |
| 210 | + // only invokes it as a function, so it is cast to AgentStep at the call site. |
| 211 | + const measureStep = async ( |
| 212 | + _input: unknown, |
| 213 | + agentExec: Execution, |
| 214 | + ): Promise<{ options: FormalSynthesisRawOption[] }> => { |
| 215 | + // Formal Tool: produce the exclusion-filtered inventory and track it. |
| 216 | + try { |
| 217 | + (agentExec as any).tools?.registerTool?.('source-inventory', new AssetPackInventoryTool()); |
| 218 | + } catch {} |
| 219 | + let inventory = request.inventory; |
| 220 | + try { |
| 221 | + const tool = (agentExec as any).tools?.getTool?.('source-inventory'); |
| 222 | + if (tool) { |
| 223 | + inventory = await tool.execute({ |
| 224 | + paths: request.inventory.paths, |
| 225 | + samples: request.inventory.samples, |
| 226 | + exclusions: request.steering.protectedIpExclusions, |
| 227 | + }); |
| 228 | + } |
| 229 | + } catch {} |
| 230 | + |
| 231 | + // Layered system prompt parts on the AgentExecution.prompt (compose upwards). |
| 232 | + try { |
| 233 | + const p = (agentExec as any).prompt; |
| 234 | + if (p?.setSpecificExecution) { |
| 235 | + p.setSpecificExecution('pipeline:asset-packs-synthesis:identity', PIPELINE_IDENTITY); |
| 236 | + p.setSpecificExecution('pipeline:asset-packs-synthesis:source-safety', PIPELINE_SOURCE_SAFETY); |
| 237 | + p.setSpecificExecution(`phase:${request.lens}:role`, LENS_ROLE[request.lens]); |
| 238 | + p.setSpecificExecution('agent:measure:catalog', buildCatalogPart(catalog)); |
| 239 | + p.setSpecificExecution( |
| 240 | + 'agent:measure:rules', |
| 241 | + buildRulesPart(request.candidateKinds, request.maxCandidates), |
| 242 | + ); |
| 243 | + p.setSpecificExecution('step:candidate:shape', buildShapePart(catalog, request.maxCandidates)); |
| 244 | + } |
| 245 | + } catch {} |
| 246 | + |
| 247 | + // The structured context the generation reasons over (becomes the user prompt). |
| 248 | + const generationInput = { |
| 249 | + repository: request.repositoryFullName, |
| 250 | + branch: request.sourceBranch ?? 'unknown', |
| 251 | + commit: request.sourceCommit ?? 'unknown', |
| 252 | + steeringInstructions: request.steering.instructions, |
| 253 | + protectedIpExclusions: request.steering.protectedIpExclusions, |
| 254 | + demandContext: request.steering.demandContext, |
| 255 | + inventoryPaths: inventory.paths, |
| 256 | + excerpts: inventory.samples, |
| 257 | + }; |
| 258 | + |
| 259 | + const generated: any = await createFailsafeGenerationSequence< |
| 260 | + typeof generationInput, |
| 261 | + { options: FormalSynthesisRawOption[] } |
| 262 | + >({ outputSchema: candidateSetSchema })(generationInput, agentExec); |
| 263 | + |
| 264 | + const out = generated?.finalOutput ?? generated?.output ?? generated; |
| 265 | + return (out && Array.isArray(out.options) ? out : { options: [] }) as { |
| 266 | + options: FormalSynthesisRawOption[]; |
| 267 | + }; |
| 268 | + }; |
| 269 | + |
| 270 | + const agent = factoryAgent<unknown, { options: FormalSynthesisRawOption[] }>({ |
| 271 | + name: 'asset-packs-measure', |
| 272 | + description: 'Measure repository source into source-safe AssetPack candidates.', |
| 273 | + steps: [measureStep as unknown as AgentStep<any, any>], |
| 274 | + }); |
| 275 | + |
| 276 | + const parsed = await agent(null, phaseExec); |
| 277 | + const { provider, model } = resolveDefaultLLMConfig(); |
| 278 | + |
| 279 | + return { |
| 280 | + options: Array.isArray((parsed as any)?.options) ? (parsed as any).options : [], |
| 281 | + provider: provider ?? null, |
| 282 | + model: model ?? null, |
| 283 | + totalTokens: sumLlmTokens(pipelineExec), |
| 284 | + }; |
| 285 | +} |
0 commit comments