Skip to content

Commit 4ced691

Browse files
wip v28
1 parent 83e6fa4 commit 4ced691

27 files changed

Lines changed: 2305 additions & 89 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,4 @@ uapi/tailwind.config.js
9191
.tmp/
9292
.vercel
9393
.env*.local
94+
.bitcode/pipeline-harness-runs/
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// @ts-nocheck
2+
import { z } from 'zod';
3+
import { Prompt } from '@bitcode/prompts/prompt';
4+
import { AgentExecution } from '../execution';
5+
import { factoryPlanStep } from '../steps/factories';
6+
import { factoryStitchUntilComplete } from '../substeps/factories';
7+
8+
describe('PTRR step prompt formatting', () => {
9+
it('formats registry-backed step prompts instead of rendering Prompt objects', async () => {
10+
process.env.BITCODE_DEBUG_ONLY_FAILSAFES = 'prepare';
11+
process.env.BITCODE_DEBUG_ONLY_GENERATIONS = 'reason';
12+
13+
const prompt = new Prompt();
14+
prompt.set('objective', 'Use source-bound evidence only.' as any);
15+
prompt.set('constraint', 'Do not invent repository facts.' as any);
16+
17+
const execution = new AgentExecution('agent:test') as any;
18+
execution.store('agent', 'name', 'test-agent');
19+
20+
const systemPrompts: string[] = [];
21+
Object.defineProperty(execution, 'llms', {
22+
configurable: true,
23+
value: {
24+
getDefaultConfig: () => ({ maxTokens: 4000 }),
25+
getDefaultLLM: () => async (input: any) => {
26+
const systemPrompt = input.messages?.[0]?.content || '';
27+
const promptBody = input.messages?.map((message: any) => message.content).join('\n') || '';
28+
systemPrompts.push(systemPrompt);
29+
30+
return {
31+
content: JSON.stringify({
32+
analysis: 'The prompt was formatted.',
33+
steps: ['read prompt parts'],
34+
conclusion: 'Continue.',
35+
confidence: 1,
36+
}),
37+
usage: {},
38+
metadata: {},
39+
};
40+
},
41+
},
42+
});
43+
44+
const plan = factoryPlanStep(z.object({ ok: z.boolean() }), { prompt });
45+
try {
46+
await plan({ read: 'Fit the deposited repository.' }, execution);
47+
48+
expect(systemPrompts.join('\n')).toContain('Use source-bound evidence only.');
49+
expect(systemPrompts.join('\n')).not.toContain('[object Object]');
50+
} finally {
51+
delete process.env.BITCODE_DEBUG_ONLY_FAILSAFES;
52+
delete process.env.BITCODE_DEBUG_ONLY_GENERATIONS;
53+
}
54+
});
55+
56+
it('renders function-backed Zod object schema keys in structured output prompts', async () => {
57+
process.env.BITCODE_DEBUG_ONLY_FAILSAFES = 'prepare';
58+
process.env.BITCODE_DEBUG_ONLY_GENERATIONS = 'structured_output';
59+
60+
const execution = new AgentExecution('agent:test') as any;
61+
execution.store('agent', 'name', 'test-agent');
62+
63+
const prompts: string[] = [];
64+
Object.defineProperty(execution, 'llms', {
65+
configurable: true,
66+
value: {
67+
getDefaultConfig: () => ({ maxTokens: 4000 }),
68+
getDefaultLLM: () => async (input: any) => {
69+
const promptBody = input.messages?.map((message: any) => message.content).join('\n') || '';
70+
prompts.push(promptBody);
71+
72+
return {
73+
content: JSON.stringify({
74+
read: { expressed_read: 'Fit the deposited repository.' },
75+
written_asset_types: ['read-satisfaction-asset-pack'],
76+
}),
77+
usage: {},
78+
metadata: {},
79+
};
80+
},
81+
},
82+
});
83+
84+
const plan = factoryPlanStep(
85+
z.object({
86+
read: z.object({ expressed_read: z.string() }),
87+
written_asset_types: z.array(z.string()).default([]),
88+
})
89+
);
90+
try {
91+
await plan({ read: 'Fit the deposited repository.' }, execution);
92+
93+
const rendered = prompts.join('\n');
94+
expect(rendered).toContain('"read"');
95+
expect(rendered).toContain('"written_asset_types"');
96+
expect(rendered).not.toContain('Output must match schema: {\n}');
97+
} finally {
98+
delete process.env.BITCODE_DEBUG_ONLY_FAILSAFES;
99+
delete process.env.BITCODE_DEBUG_ONLY_GENERATIONS;
100+
}
101+
});
102+
103+
it('preserves original task context when stitch retries an incomplete partial output', async () => {
104+
const execution = new AgentExecution('agent:test') as any;
105+
const seenInputs: any[] = [];
106+
const stitch = factoryStitchUntilComplete(
107+
[
108+
async (input: any) => {
109+
seenInputs.push(input);
110+
return {
111+
output: {
112+
read: {
113+
expressed_read: input.context.read,
114+
},
115+
},
116+
};
117+
},
118+
],
119+
z.object({
120+
read: z.object({ expressed_read: z.string() }),
121+
})
122+
);
123+
124+
const result = await stitch(
125+
{
126+
read: 'Fit the deposited repository.',
127+
sourceRevision: {
128+
repositoryFullName: 'engineeredsoftware/ENGI',
129+
branch: 'main',
130+
commit: '07de275b3d97679321f1f596c16e48105d81d51b',
131+
},
132+
output: {},
133+
},
134+
execution
135+
);
136+
137+
expect(seenInputs[0].context).toMatchObject({
138+
read: 'Fit the deposited repository.',
139+
sourceRevision: {
140+
repositoryFullName: 'engineeredsoftware/ENGI',
141+
branch: 'main',
142+
commit: '07de275b3d97679321f1f596c16e48105d81d51b',
143+
},
144+
});
145+
expect(result.finalOutput.read.expressed_read).toBe('Fit the deposited repository.');
146+
});
147+
});

packages/agent-generics/src/steps/factories.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ import { z } from 'zod';
4343
import { logStepTrace, logStepStart, logStepError } from '../diagnostics/instrumentation';
4444
import { createFailsafeGenerationSequence } from './failsafe-sequence';
4545

46+
function formatStepPromptCarrier(prompt: any): any {
47+
if (!prompt) return prompt;
48+
49+
const explicitPurpose = prompt.get?.('step:purpose');
50+
if (explicitPurpose) return explicitPurpose;
51+
52+
if (typeof prompt.format === 'function') {
53+
return prompt.format();
54+
}
55+
56+
return prompt;
57+
}
58+
4659
function normalizeToolUsage(usedTools: UsedTool[] | undefined): ToolUsageUpdate[] {
4760
return (usedTools || []).map((tool) => ({
4861
name: tool.tool,
@@ -148,7 +161,7 @@ export function factoryPlanStep<TInput, TOutput>(
148161
const started = Date.now();
149162
try { logStepStart(stepExec, 'plan'); } catch {}
150163
if (config?.prompt) {
151-
const part = config.prompt.get?.('step:purpose') ?? config.prompt;
164+
const part = formatStepPromptCarrier(config.prompt);
152165
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', part);
153166
}
154167
try {
@@ -234,7 +247,7 @@ export function factoryTryStep<TInput, TOutput>(
234247
const started = Date.now();
235248
try { logStepStart(stepExec, 'try'); } catch {}
236249
if (options?.prompt) {
237-
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', options.prompt.get?.('step:purpose') || options.prompt);
250+
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', formatStepPromptCarrier(options.prompt));
238251
}
239252
try {
240253
// Record usable tools
@@ -317,7 +330,7 @@ export function factoryRefineStep<TInput, TOutput>(
317330
const started = Date.now();
318331
try { logStepStart(stepExec, 'refine'); } catch {}
319332
if (options?.prompt) {
320-
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', options.prompt.get?.('step:purpose') || options.prompt);
333+
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', formatStepPromptCarrier(options.prompt));
321334
}
322335
try {
323336
// Record usable tools
@@ -404,7 +417,7 @@ export function factoryRetryStep<TInput, TOutput>(
404417
const started = Date.now();
405418
try { logStepStart(stepExec, 'retry'); } catch {}
406419
if (options?.prompt) {
407-
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', options.prompt.get?.('step:purpose') || options.prompt);
420+
stepExec.prompt.setSpecificExecution('specific_execution:step:purpose', formatStepPromptCarrier(options.prompt));
408421
}
409422
try {
410423
// Record usable tools

packages/agent-generics/src/substeps/factories.ts

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ export function factoryStitchUntilComplete<T>(
615615
? (currentResult as any).output
616616
: currentResult;
617617
const stitchInput = {
618+
context: buildStitchContext(input),
618619
partialOutput: minimalPartial,
619620
instruction: 'Continue and complete the previous output'
620621
} as any;
@@ -644,7 +645,12 @@ export function factoryStitchUntilComplete<T>(
644645
throw error;
645646
}
646647

647-
return { ...currentResult, finalOutput: (currentResult as any).output ?? currentResult };
648+
const finalOutput = (currentResult as any).output ?? currentResult;
649+
return {
650+
context: buildStitchContext(input),
651+
output: finalOutput,
652+
finalOutput,
653+
} as any;
648654
};
649655
}
650656

@@ -702,7 +708,10 @@ export function factoryReason<T>(): Executor<T, T & { reasoning: Reasoning }> {
702708
const isSum = typedInput.chunkResults !== undefined;
703709

704710
if (isStitch) {
705-
return `Continue reasoning from this partial output:\n\n${JSON.stringify(typedInput.partialOutput, null, 2)}`;
711+
const context = typedInput.context && Object.keys(typedInput.context).length
712+
? `\n\nOriginal task context:\n\n${JSON.stringify(typedInput.context, null, 2)}`
713+
: '';
714+
return `Continue reasoning from this partial output:\n\n${JSON.stringify(typedInput.partialOutput, null, 2)}${context}`;
706715
}
707716
if (isSum) {
708717
return `Reason about how to combine these chunk results:\n\n${JSON.stringify(typedInput.chunkResults, null, 2)}`;
@@ -780,9 +789,9 @@ export function factoryStructuredOutput<T, TSchema>(
780789
// Infer a minimal shape string from a Zod schema when description is missing
781790
function inferSchemaShape(s: z.ZodTypeAny): string {
782791
try {
783-
const def: any = (s as any)._def;
784-
if (def?.typeName === z.ZodFirstPartyTypeKind.ZodObject && def.shape) {
785-
const entries = Object.entries(def.shape as Record<string, z.ZodTypeAny>);
792+
const shape = getZodObjectShape(s);
793+
if (shape) {
794+
const entries = Object.entries(shape);
786795
const shapeLines = entries.map(([k, v]) => ` "${k}": ${inferField(v)}`);
787796
return `{
788797
${shapeLines.join(',\n')}
@@ -798,38 +807,57 @@ function inferField(v: z.ZodTypeAny): string {
798807
case z.ZodFirstPartyTypeKind.ZodString: return 'string';
799808
case z.ZodFirstPartyTypeKind.ZodNumber: return 'number';
800809
case z.ZodFirstPartyTypeKind.ZodBoolean: return 'boolean';
810+
case z.ZodFirstPartyTypeKind.ZodAny: return 'any';
811+
case z.ZodFirstPartyTypeKind.ZodLiteral: return JSON.stringify((v as any)?._def?.value);
801812
case z.ZodFirstPartyTypeKind.ZodArray: {
802813
const inner = (v as any)?._def?.type;
803814
return `[ ${inner ? inferField(inner) : 'any'} ]`;
804815
}
805-
case z.ZodFirstPartyTypeKind.ZodObject: return '{ ... }';
816+
case z.ZodFirstPartyTypeKind.ZodObject: {
817+
const shape = getZodObjectShape(v);
818+
if (!shape) return '{ ... }';
819+
const keys = Object.entries(shape).slice(0, 6);
820+
return `{ ${keys.map(([key, value]) => `"${key}": ${inferField(value)}`).join(', ')}${Object.keys(shape).length > keys.length ? ', ...' : ''} }`;
821+
}
806822
case z.ZodFirstPartyTypeKind.ZodRecord: return '{ [key: string]: any }';
807-
case z.ZodFirstPartyTypeKind.ZodEnum: return 'enum';
823+
case z.ZodFirstPartyTypeKind.ZodEnum: {
824+
const values = (v as any)?._def?.values;
825+
return Array.isArray(values) && values.length
826+
? values.map((value) => JSON.stringify(value)).join(' | ')
827+
: 'enum';
828+
}
829+
case z.ZodFirstPartyTypeKind.ZodUnion: {
830+
const options = (v as any)?._def?.options;
831+
return Array.isArray(options) ? options.map(inferField).join(' | ') : 'any';
832+
}
808833
case z.ZodFirstPartyTypeKind.ZodOptional: return `${inferField((v as any)?._def?.innerType)}?`;
834+
case z.ZodFirstPartyTypeKind.ZodDefault: return `${inferField((v as any)?._def?.innerType)} = default`;
835+
case z.ZodFirstPartyTypeKind.ZodNullable: return `${inferField((v as any)?._def?.innerType)} | null`;
809836
default: return 'any';
810837
}
811838
}
812839

813840
function inferTopLevelKeys(s: z.ZodTypeAny): string[] {
814841
try {
815-
const def: any = (s as any)._def;
816-
if (def?.typeName === z.ZodFirstPartyTypeKind.ZodObject && def.shape) {
817-
return Object.keys(def.shape as Record<string, z.ZodTypeAny>);
842+
const shape = getZodObjectShape(s);
843+
if (shape) {
844+
return Object.keys(shape);
818845
}
819846
} catch { }
820847
return [];
821848
}
822849

823850
function buildCoercedBySchema(s: z.ZodTypeAny): any {
824851
try {
825-
const def: any = (s as any)._def;
826-
if (def?.typeName === z.ZodFirstPartyTypeKind.ZodObject && def.shape) {
827-
const shape = def.shape as Record<string, z.ZodTypeAny>;
852+
const shape = getZodObjectShape(s);
853+
if (shape) {
828854
const out: any = {};
829855
for (const [k, v] of Object.entries(shape)) {
830856
const t = (v as any)?._def?.typeName;
831857
const optional = t === z.ZodFirstPartyTypeKind.ZodOptional;
832-
const inner = optional ? (v as any)?._def?.innerType : v;
858+
const defaulted = t === z.ZodFirstPartyTypeKind.ZodDefault;
859+
const nullable = t === z.ZodFirstPartyTypeKind.ZodNullable;
860+
const inner = optional || defaulted || nullable ? (v as any)?._def?.innerType : v;
833861
const typeName = (inner as any)?._def?.typeName;
834862
if (optional) continue; // skip optional
835863
switch (typeName) {
@@ -853,6 +881,50 @@ function buildCoercedBySchema(s: z.ZodTypeAny): any {
853881
return {};
854882
}
855883

884+
function getZodObjectShape(s: z.ZodTypeAny): Record<string, z.ZodTypeAny> | null {
885+
try {
886+
const def: any = (s as any)?._def;
887+
if (def?.typeName !== z.ZodFirstPartyTypeKind.ZodObject) return null;
888+
const rawShape = def.shape;
889+
const shape = typeof rawShape === 'function' ? rawShape() : rawShape;
890+
if (!shape || typeof shape !== 'object' || Array.isArray(shape)) return null;
891+
return shape as Record<string, z.ZodTypeAny>;
892+
} catch {
893+
return null;
894+
}
895+
}
896+
897+
function buildStitchContext(input: unknown): Record<string, unknown> {
898+
if (!input || typeof input !== 'object') return {};
899+
const value = input as any;
900+
const context: Record<string, unknown> = {};
901+
const put = (key: string, candidate: unknown) => {
902+
if (candidate !== undefined && candidate !== null && candidate !== '') {
903+
context[key] = candidate;
904+
}
905+
};
906+
907+
if (value.context && typeof value.context === 'object' && !Array.isArray(value.context)) {
908+
for (const [key, candidate] of Object.entries(value.context)) {
909+
put(key, candidate);
910+
}
911+
}
912+
913+
put('read', value.read ?? value.expressedRead ?? value.definitionOfRead ?? value.context?.read);
914+
put('definitionOfRead', value.definitionOfRead ?? value.context?.definitionOfRead);
915+
put('repository', value.repository ?? value.sourceRevision?.repository ?? value.sourceRevision?.repositoryFullName ?? value.context?.repository);
916+
put('sourceRevision', value.sourceRevision ?? value.context?.sourceRevision);
917+
put('fitResult', value.fitResult ?? value.depositorySearchResult ?? value.context?.fitResult);
918+
put('assetPackIntent', value.assetPackIntent ?? value.context?.assetPackIntent);
919+
put('deliveryMechanism', value.deliveryMechanism ?? value.context?.deliveryMechanism);
920+
if (Array.isArray(value.preparedContexts)) {
921+
put('preparedContextCount', value.preparedContexts.length);
922+
put('preparedContextSummaries', value.preparedContexts.slice(0, 3).map(summarize));
923+
}
924+
925+
return context;
926+
}
927+
856928
// ==================== TOOL SUBSTEP ====================
857929

858930
// ==================== TOOL & VALIDATION SUBSTEPS ====================

packages/generic-llms/src/defaults.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function resolveDefaultLLMModel(
3131
return 'claude-3-opus-20240229';
3232
case 'openai':
3333
default:
34-
return 'gpt-4';
34+
return 'gpt-4.1-mini';
3535
}
3636
}
3737

0 commit comments

Comments
 (0)