sidecar-cli 0.1.4 → 0.1.5-beta.1
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/README.md +23 -0
- package/dist/cli.js +6 -2
- package/dist/prompts/prompt-compiler.js +117 -14
- package/dist/prompts/prompt-service.js +9 -4
- package/dist/runs/run-record.js +16 -0
- package/dist/runs/run-repository.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,6 +179,29 @@ Artifacts:
|
|
|
179
179
|
- `sidecar artifact add <path> [--kind file|doc|screenshot|other] [--note <text>] [--json]`
|
|
180
180
|
- `sidecar artifact list [--json]`
|
|
181
181
|
|
|
182
|
+
## Automatic prompt token budgeting
|
|
183
|
+
|
|
184
|
+
When Sidecar compiles task prompts (`sidecar prompt compile` and run execution flows), it automatically applies a token budget to reduce context size without degrading execution quality.
|
|
185
|
+
|
|
186
|
+
Current behavior:
|
|
187
|
+
|
|
188
|
+
- Keeps required sections intact (task, objective, constraints, validation, definition of done).
|
|
189
|
+
- Deduplicates repeated list items.
|
|
190
|
+
- Trims only optional high-volume sections when needed (for example: in-scope lists, linked notes/decisions, long file lists).
|
|
191
|
+
- Adds compact overflow lines such as `+ N more ... (see task packet for full list)`.
|
|
192
|
+
|
|
193
|
+
Current defaults:
|
|
194
|
+
|
|
195
|
+
- Target budget: ~1200 estimated tokens
|
|
196
|
+
- Safety ceiling: ~1500 estimated tokens
|
|
197
|
+
|
|
198
|
+
Prompt optimization data is included in compile output and stored on run records:
|
|
199
|
+
|
|
200
|
+
- `prompt_tokens_estimated_before`
|
|
201
|
+
- `prompt_tokens_estimated_after`
|
|
202
|
+
- `prompt_budget_target`
|
|
203
|
+
- `prompt_trimmed_sections`
|
|
204
|
+
|
|
182
205
|
## Example workflow
|
|
183
206
|
|
|
184
207
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -416,6 +416,7 @@ program
|
|
|
416
416
|
'Documentation: https://usesidecar.dev/',
|
|
417
417
|
...(resolvedInstructions ? ['', `Loaded instructions.md from ${resolvedInstructions.sourceLabel}`] : []),
|
|
418
418
|
]);
|
|
419
|
+
maybePrintUpdateNotice();
|
|
419
420
|
}
|
|
420
421
|
catch (err) {
|
|
421
422
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -974,11 +975,16 @@ prompt
|
|
|
974
975
|
runner_type: compiled.runner_type,
|
|
975
976
|
agent_role: compiled.agent_role,
|
|
976
977
|
prompt_path: compiled.prompt_path,
|
|
978
|
+
prompt_optimization: compiled.prompt_optimization,
|
|
977
979
|
preview: opts.preview ? compiled.prompt_markdown : null,
|
|
978
980
|
}, [
|
|
979
981
|
`Compiled prompt for ${compiled.task_id}.`,
|
|
980
982
|
`Run: ${compiled.run_id}`,
|
|
981
983
|
`Path: ${compiled.prompt_path}`,
|
|
984
|
+
`Prompt estimate: ${compiled.prompt_optimization.estimated_tokens_before} -> ${compiled.prompt_optimization.estimated_tokens_after} tokens (target ${compiled.prompt_optimization.budget_target})`,
|
|
985
|
+
...(compiled.prompt_optimization.trimmed_sections.length > 0
|
|
986
|
+
? [`Trimmed: ${compiled.prompt_optimization.trimmed_sections.join(', ')}`]
|
|
987
|
+
: []),
|
|
982
988
|
...(opts.preview ? ['', compiled.prompt_markdown] : []),
|
|
983
989
|
]);
|
|
984
990
|
}
|
|
@@ -1353,7 +1359,6 @@ if (process.argv.length === 2) {
|
|
|
1353
1359
|
console.log('');
|
|
1354
1360
|
}
|
|
1355
1361
|
program.outputHelp();
|
|
1356
|
-
maybePrintUpdateNotice();
|
|
1357
1362
|
process.exit(0);
|
|
1358
1363
|
}
|
|
1359
1364
|
if (process.argv[2] === 'run' &&
|
|
@@ -1369,4 +1374,3 @@ if (process.argv[2] === 'run' &&
|
|
|
1369
1374
|
process.argv.splice(2, 1, 'run-exec');
|
|
1370
1375
|
}
|
|
1371
1376
|
program.parse(process.argv);
|
|
1372
|
-
maybePrintUpdateNotice();
|
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { nowIso } from '../lib/format.js';
|
|
4
4
|
import { getSidecarPaths } from '../lib/paths.js';
|
|
5
|
+
const PROMPT_BUDGET_TARGET = 1200;
|
|
6
|
+
const PROMPT_BUDGET_MAX = 1500;
|
|
5
7
|
function section(title, lines) {
|
|
6
8
|
return [`## ${title}`, ...lines, ''].join('\n');
|
|
7
9
|
}
|
|
@@ -40,8 +42,44 @@ function runnerGuidance(runner) {
|
|
|
40
42
|
'Provide a clear summary with validation and follow-up notes at the end.',
|
|
41
43
|
];
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
function estimateTokens(text) {
|
|
46
|
+
return Math.ceil(text.length / 4);
|
|
47
|
+
}
|
|
48
|
+
function dedupe(items) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const item of items.map((v) => v.trim()).filter(Boolean)) {
|
|
52
|
+
const key = item.toLowerCase();
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
out.push(item);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function withOverflow(items, fullCount, label) {
|
|
61
|
+
const overflow = fullCount - items.length;
|
|
62
|
+
if (overflow <= 0)
|
|
63
|
+
return items;
|
|
64
|
+
return [...items, `+ ${overflow} more ${label} (see task packet for full list)`];
|
|
65
|
+
}
|
|
66
|
+
function buildPromptLists(input) {
|
|
67
|
+
const { task, linkedContext } = input;
|
|
68
|
+
return {
|
|
69
|
+
inScope: dedupe(task.scope.in_scope),
|
|
70
|
+
outOfScope: dedupe(task.scope.out_of_scope),
|
|
71
|
+
filesToRead: dedupe(task.implementation.files_to_read),
|
|
72
|
+
filesToAvoid: dedupe(task.implementation.files_to_avoid),
|
|
73
|
+
relatedDecisions: dedupe(linkedContext?.related_decisions ?? task.context.related_decisions),
|
|
74
|
+
relatedNotes: dedupe(linkedContext?.related_notes ?? task.context.related_notes),
|
|
75
|
+
technicalConstraints: dedupe(task.constraints.technical),
|
|
76
|
+
designConstraints: dedupe(task.constraints.design),
|
|
77
|
+
validationCommands: dedupe(task.execution.commands.validation),
|
|
78
|
+
definitionOfDone: dedupe(task.definition_of_done),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function renderPrompt(input, lists) {
|
|
82
|
+
const { task, run, runner, agentRole } = input;
|
|
45
83
|
const lines = [];
|
|
46
84
|
lines.push('# Sidecar Execution Brief');
|
|
47
85
|
lines.push('');
|
|
@@ -59,26 +97,91 @@ export function compilePromptMarkdown(input) {
|
|
|
59
97
|
]));
|
|
60
98
|
lines.push(section('Objective', [task.goal]));
|
|
61
99
|
lines.push(section('Why this matters', [task.summary]));
|
|
62
|
-
lines.push(section('In scope', bullets(
|
|
63
|
-
lines.push(section('Out of scope', bullets(
|
|
64
|
-
lines.push(section('Read these first', bullets(
|
|
65
|
-
lines.push(section('Avoid changing', bullets(
|
|
66
|
-
const relatedDecisions = linkedContext?.related_decisions ?? task.context.related_decisions;
|
|
67
|
-
const relatedNotes = linkedContext?.related_notes ?? task.context.related_notes;
|
|
100
|
+
lines.push(section('In scope', bullets(lists.inScope)));
|
|
101
|
+
lines.push(section('Out of scope', bullets(lists.outOfScope)));
|
|
102
|
+
lines.push(section('Read these first', bullets(lists.filesToRead)));
|
|
103
|
+
lines.push(section('Avoid changing', bullets(lists.filesToAvoid)));
|
|
68
104
|
lines.push(section('Linked context', [
|
|
69
|
-
...bullets(relatedDecisions, '- no related decisions'),
|
|
70
|
-
...bullets(relatedNotes, '- no related notes'),
|
|
105
|
+
...bullets(lists.relatedDecisions, '- no related decisions'),
|
|
106
|
+
...bullets(lists.relatedNotes, '- no related notes'),
|
|
71
107
|
]));
|
|
72
108
|
lines.push(section('Constraints', [
|
|
73
|
-
...bullets(
|
|
74
|
-
...bullets(
|
|
109
|
+
...bullets(lists.technicalConstraints, '- no technical constraints'),
|
|
110
|
+
...bullets(lists.designConstraints, '- no design constraints'),
|
|
75
111
|
]));
|
|
76
|
-
lines.push(section('Validation', bullets(
|
|
77
|
-
lines.push(section('Definition of done', bullets(
|
|
112
|
+
lines.push(section('Validation', bullets(lists.validationCommands)));
|
|
113
|
+
lines.push(section('Definition of done', bullets(lists.definitionOfDone)));
|
|
78
114
|
lines.push(section('Runner guidance', runnerGuidance(runner)));
|
|
79
115
|
lines.push(section('Final response format', finalResponseFormat(runner)));
|
|
80
116
|
return `${lines.join('\n').trim()}\n`;
|
|
81
117
|
}
|
|
118
|
+
function applyPromptBudget(input) {
|
|
119
|
+
const baseline = buildPromptLists(input);
|
|
120
|
+
const baselineMarkdown = renderPrompt(input, baseline);
|
|
121
|
+
const baselineTokens = estimateTokens(baselineMarkdown);
|
|
122
|
+
if (baselineTokens <= PROMPT_BUDGET_TARGET) {
|
|
123
|
+
return {
|
|
124
|
+
optimizedLists: baseline,
|
|
125
|
+
metadata: {
|
|
126
|
+
estimated_tokens_before: baselineTokens,
|
|
127
|
+
estimated_tokens_after: baselineTokens,
|
|
128
|
+
budget_target: PROMPT_BUDGET_TARGET,
|
|
129
|
+
budget_max: PROMPT_BUDGET_MAX,
|
|
130
|
+
trimmed_sections: [],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const optimized = {
|
|
135
|
+
...baseline,
|
|
136
|
+
inScope: withOverflow(baseline.inScope.slice(0, 8), baseline.inScope.length, 'in-scope items'),
|
|
137
|
+
outOfScope: withOverflow(baseline.outOfScope.slice(0, 5), baseline.outOfScope.length, 'out-of-scope items'),
|
|
138
|
+
filesToRead: withOverflow(baseline.filesToRead.slice(0, 10), baseline.filesToRead.length, 'read-first files'),
|
|
139
|
+
filesToAvoid: withOverflow(baseline.filesToAvoid.slice(0, 5), baseline.filesToAvoid.length, 'avoid files'),
|
|
140
|
+
relatedDecisions: withOverflow(baseline.relatedDecisions.slice(0, 3), baseline.relatedDecisions.length, 'decisions'),
|
|
141
|
+
relatedNotes: withOverflow(baseline.relatedNotes.slice(0, 2), baseline.relatedNotes.length, 'notes'),
|
|
142
|
+
};
|
|
143
|
+
let optimizedMarkdown = renderPrompt(input, optimized);
|
|
144
|
+
let optimizedTokens = estimateTokens(optimizedMarkdown);
|
|
145
|
+
// Safety valve for unusually large tasks: preserve must-have sections and thin optional context further.
|
|
146
|
+
if (optimizedTokens > PROMPT_BUDGET_MAX) {
|
|
147
|
+
optimized.relatedDecisions = withOverflow(baseline.relatedDecisions.slice(0, 1), baseline.relatedDecisions.length, 'decisions');
|
|
148
|
+
optimized.relatedNotes = withOverflow([], baseline.relatedNotes.length, 'notes');
|
|
149
|
+
optimized.outOfScope = withOverflow(baseline.outOfScope.slice(0, 3), baseline.outOfScope.length, 'out-of-scope items');
|
|
150
|
+
optimized.filesToAvoid = withOverflow(baseline.filesToAvoid.slice(0, 3), baseline.filesToAvoid.length, 'avoid files');
|
|
151
|
+
optimizedMarkdown = renderPrompt(input, optimized);
|
|
152
|
+
optimizedTokens = estimateTokens(optimizedMarkdown);
|
|
153
|
+
}
|
|
154
|
+
const trimmedSections = [];
|
|
155
|
+
if (optimized.inScope.length < baseline.inScope.length)
|
|
156
|
+
trimmedSections.push('in_scope');
|
|
157
|
+
if (optimized.outOfScope.length < baseline.outOfScope.length)
|
|
158
|
+
trimmedSections.push('out_of_scope');
|
|
159
|
+
if (optimized.filesToRead.length < baseline.filesToRead.length)
|
|
160
|
+
trimmedSections.push('files_to_read');
|
|
161
|
+
if (optimized.filesToAvoid.length < baseline.filesToAvoid.length)
|
|
162
|
+
trimmedSections.push('files_to_avoid');
|
|
163
|
+
if (optimized.relatedDecisions.length < baseline.relatedDecisions.length)
|
|
164
|
+
trimmedSections.push('related_decisions');
|
|
165
|
+
if (optimized.relatedNotes.length < baseline.relatedNotes.length)
|
|
166
|
+
trimmedSections.push('related_notes');
|
|
167
|
+
return {
|
|
168
|
+
optimizedLists: optimized,
|
|
169
|
+
metadata: {
|
|
170
|
+
estimated_tokens_before: baselineTokens,
|
|
171
|
+
estimated_tokens_after: optimizedTokens,
|
|
172
|
+
budget_target: PROMPT_BUDGET_TARGET,
|
|
173
|
+
budget_max: PROMPT_BUDGET_MAX,
|
|
174
|
+
trimmed_sections: trimmedSections,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function compilePromptMarkdown(input) {
|
|
179
|
+
const optimized = applyPromptBudget(input);
|
|
180
|
+
return {
|
|
181
|
+
markdown: renderPrompt(input, optimized.optimizedLists),
|
|
182
|
+
metadata: optimized.metadata,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
82
185
|
export function saveCompiledPrompt(rootPath, runId, markdown) {
|
|
83
186
|
const promptsPath = getSidecarPaths(rootPath).promptsPath;
|
|
84
187
|
fs.mkdirSync(promptsPath, { recursive: true });
|
|
@@ -11,18 +11,22 @@ export function compileTaskPrompt(input) {
|
|
|
11
11
|
branch: task.tracking.branch,
|
|
12
12
|
worktree: task.tracking.worktree,
|
|
13
13
|
});
|
|
14
|
-
const
|
|
14
|
+
const compiledPrompt = compilePromptMarkdown({
|
|
15
15
|
task,
|
|
16
16
|
run: created.run,
|
|
17
17
|
runner: input.runner,
|
|
18
18
|
agentRole: input.agentRole,
|
|
19
19
|
linkedContext: input.linkedContext,
|
|
20
20
|
});
|
|
21
|
-
const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id,
|
|
21
|
+
const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id, compiledPrompt.markdown);
|
|
22
22
|
updateRunRecordEntry(input.rootPath, created.run.run_id, {
|
|
23
23
|
status: 'queued',
|
|
24
24
|
prompt_path: promptPath,
|
|
25
|
-
|
|
25
|
+
prompt_tokens_estimated_before: compiledPrompt.metadata.estimated_tokens_before,
|
|
26
|
+
prompt_tokens_estimated_after: compiledPrompt.metadata.estimated_tokens_after,
|
|
27
|
+
prompt_budget_target: compiledPrompt.metadata.budget_target,
|
|
28
|
+
prompt_trimmed_sections: compiledPrompt.metadata.trimmed_sections,
|
|
29
|
+
summary: `Compiled prompt for task ${task.task_id} (${compiledPrompt.metadata.estimated_tokens_before} -> ${compiledPrompt.metadata.estimated_tokens_after} est. tokens)`,
|
|
26
30
|
});
|
|
27
31
|
return {
|
|
28
32
|
run_id: created.run.run_id,
|
|
@@ -30,6 +34,7 @@ export function compileTaskPrompt(input) {
|
|
|
30
34
|
runner_type: input.runner,
|
|
31
35
|
agent_role: input.agentRole,
|
|
32
36
|
prompt_path: promptPath,
|
|
33
|
-
prompt_markdown:
|
|
37
|
+
prompt_markdown: compiledPrompt.markdown,
|
|
38
|
+
prompt_optimization: compiledPrompt.metadata,
|
|
34
39
|
};
|
|
35
40
|
}
|
package/dist/runs/run-record.js
CHANGED
|
@@ -28,6 +28,10 @@ export const runRecordSchema = z
|
|
|
28
28
|
reviewed_at: z.string().datetime({ offset: true }).nullable().default(null),
|
|
29
29
|
reviewed_by: z.string().default(''),
|
|
30
30
|
review_note: z.string().default(''),
|
|
31
|
+
prompt_tokens_estimated_before: z.number().int().nonnegative().default(0),
|
|
32
|
+
prompt_tokens_estimated_after: z.number().int().nonnegative().default(0),
|
|
33
|
+
prompt_budget_target: z.number().int().nonnegative().default(0),
|
|
34
|
+
prompt_trimmed_sections: z.array(z.string()).default([]),
|
|
31
35
|
})
|
|
32
36
|
.strict();
|
|
33
37
|
export const runRecordCreateInputSchema = runRecordSchema
|
|
@@ -49,6 +53,10 @@ export const runRecordCreateInputSchema = runRecordSchema
|
|
|
49
53
|
reviewed_at: true,
|
|
50
54
|
reviewed_by: true,
|
|
51
55
|
review_note: true,
|
|
56
|
+
prompt_tokens_estimated_before: true,
|
|
57
|
+
prompt_tokens_estimated_after: true,
|
|
58
|
+
prompt_budget_target: true,
|
|
59
|
+
prompt_trimmed_sections: true,
|
|
52
60
|
});
|
|
53
61
|
export const runRecordUpdateInputSchema = z
|
|
54
62
|
.object({
|
|
@@ -67,6 +75,10 @@ export const runRecordUpdateInputSchema = z
|
|
|
67
75
|
reviewed_at: z.string().datetime({ offset: true }).nullable().optional(),
|
|
68
76
|
reviewed_by: z.string().optional(),
|
|
69
77
|
review_note: z.string().optional(),
|
|
78
|
+
prompt_tokens_estimated_before: z.number().int().nonnegative().optional(),
|
|
79
|
+
prompt_tokens_estimated_after: z.number().int().nonnegative().optional(),
|
|
80
|
+
prompt_budget_target: z.number().int().nonnegative().optional(),
|
|
81
|
+
prompt_trimmed_sections: z.array(z.string()).optional(),
|
|
70
82
|
})
|
|
71
83
|
.strict();
|
|
72
84
|
export function createRunRecord(runId, input) {
|
|
@@ -92,6 +104,10 @@ export function createRunRecord(runId, input) {
|
|
|
92
104
|
reviewed_at: input.reviewed_at ?? null,
|
|
93
105
|
reviewed_by: input.reviewed_by ?? '',
|
|
94
106
|
review_note: input.review_note ?? '',
|
|
107
|
+
prompt_tokens_estimated_before: input.prompt_tokens_estimated_before ?? 0,
|
|
108
|
+
prompt_tokens_estimated_after: input.prompt_tokens_estimated_after ?? 0,
|
|
109
|
+
prompt_budget_target: input.prompt_budget_target ?? 0,
|
|
110
|
+
prompt_trimmed_sections: input.prompt_trimmed_sections ?? [],
|
|
95
111
|
};
|
|
96
112
|
return runRecordSchema.parse(normalized);
|
|
97
113
|
}
|
|
@@ -53,6 +53,7 @@ export class RunRecordRepository {
|
|
|
53
53
|
validation_results: parsedPatch.validation_results ?? existing.validation_results,
|
|
54
54
|
blockers: parsedPatch.blockers ?? existing.blockers,
|
|
55
55
|
follow_ups: parsedPatch.follow_ups ?? existing.follow_ups,
|
|
56
|
+
prompt_trimmed_sections: parsedPatch.prompt_trimmed_sections ?? existing.prompt_trimmed_sections,
|
|
56
57
|
};
|
|
57
58
|
const validated = runRecordSchema.parse(merged);
|
|
58
59
|
const filePath = runFilePath(this.runsPath, runId);
|