Skip to content

Commit 2f024c6

Browse files
peterschmidt85Andrey Cheptsovclaude
authored
Write preset traces by default and add the dstack-presets skill (#4192)
Every preset session now writes the real-time trace to ~/.dstack/presets/<id>/trace.jsonl, along with the agent prompt and the final report copy; the --debug flag is removed. A new dstack-presets skill teaches AI agents to create and manage presets; it is not bundled into the creation agent. Co-authored-by: Andrey Cheptsov <andrey.cheptsov@github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 511c318 commit 2f024c6

13 files changed

Lines changed: 117 additions & 116 deletions

File tree

mkdocs/docs/concepts/presets.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Alternatively, pass `--fleet` to `dstack apply`.
122122

123123
=== "Base"
124124

125-
Set `base` to let the creation agent select any compatible variant of the base model, including a different precision, quantization, or trusted fork.
125+
Set `base` to let the agent select any compatible variant of the base model, including a different precision, quantization, or trusted fork.
126126

127127
```yaml
128128
base: Qwen/Qwen2.5-7B-Instruct
@@ -240,6 +240,24 @@ Submit the run dsv4-flash? [y/n]: y
240240

241241
## Manage presets
242242

243+
### Watch presets
244+
245+
While a preset is being created, you can watch the progress of its trials and what the agent is doing.
246+
247+
The `dstack preset logs` command shows the progress log: one line per milestone, such as a trial finishing or the final service being verified. Pass `-f` to follow a running creation:
248+
249+
<div class="termy">
250+
251+
```shell
252+
$ dstack preset logs -f c83375b4
253+
```
254+
255+
</div>
256+
257+
### Traces
258+
259+
The agent subprocess writes real-time traces to `~/.dstack/presets/<id>/trace.jsonl`: the agent's messages and every tool call with its result. Traces are the main way to analyze a session in depth — see [Protips](#protips).
260+
243261
### List presets
244262

245263
Use `dstack preset` to list presets:
@@ -290,19 +308,14 @@ $ dstack preset delete c83375b4
290308
!!! info "Reference"
291309
For command options and agent settings, see the [`dstack preset` CLI reference](../reference/cli/dstack/preset.md).
292310

293-
## Troubleshooting
311+
## Protips
294312

295-
To trace the agent's activity, pass `--debug` to `dstack apply`:
296-
297-
<div class="termy">
313+
Under the hood, presets run an agent as a subprocess, using the local `claude` CLI. This process writes a real-time trace to `~/.dstack/presets/<id>/trace.jsonl`. The subprocess is launched with a built-in harness: how to run trials, submit runs, benchmark, verify presets, and use `dstack`.
298314

299-
```shell
300-
$ dstack apply -f preset.dstack.yml --debug
301-
```
302-
303-
</div>
315+
At the same time, it's recommended to create presets using your own agent — either via a CLI such as Claude Code, or inside your IDE. Your agent helps you design the preset configuration, formulate hypotheses, and — most importantly — analyze the session's traces as well as the trial results (stored under `~/.dstack/presets/<id>/trials/<n>/trial.json`), to decide what the next session can be and what instructions to give it via `prompt`.
304316

305-
The trace is written to `~/.dstack/presets/<id>/trace.jsonl` while the session runs. It contains the agent's messages and every tool call with its result.
317+
> To help your agent use `dstack` and presets, install the [`dstack`](https://skills.sh/dstackai/dstack/dstack)
318+
> and [`dstack-presets`](https://skills.sh/dstackai/dstack/dstack-presets) skills with `npx skills add dstackai/dstack`.
306319
307320
## Limitations
308321

mkdocs/docs/reference/cli/dstack/preset.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ Preset creation uses the existing `claude` login unless
4949
| `DSTACK_AGENT_CLAUDE_EFFORT` | Claude effort level: `low`, `medium`, `high`, `xhigh`, or `max`. If unset, the `claude` CLI default is used. |
5050

5151
Agent progress is written to `agent.log` under `~/.dstack/presets/<preset-id>/`,
52-
alongside the effective configuration (`preset.dstack.yml`) and the recorded
53-
trials (`trials.jsonl`). Pass `--debug` to also save the agent prompt
54-
(`prompt.md`) and raw trace (`trace.jsonl`).
52+
alongside the effective configuration (`preset.dstack.yml`), the recorded
53+
trials, the agent prompt (`prompt.md`), and the real-time trace
54+
(`trace.jsonl`).
5555

5656
## dstack preset logs
5757

skills/dstack-presets/SKILL.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
name: dstack-presets
3+
description: |
4+
Create and manage dstack presets: a toolkit that streamlines model inference optimization with agents, and a portable preset format. Use together with the dstack skill, and only when the user explicitly asks to create a preset or manage existing presets, not for deploying or serving a model.
5+
---
6+
7+
# dstack Presets
8+
9+
Use `/dstack` for CLI commands, YAML fields, apply behavior, fleets, and other
10+
dstack syntax. This skill covers creating and managing presets.
11+
12+
## Overview
13+
14+
Presets offer two things: a toolkit that streamlines model inference optimization using agents, and a portable format that deploys the final preset to any cloud, Kubernetes cluster, or bare-metal fleet. A preset holds the serving configuration that produced the result, the benchmark it reached, and the exact hardware it was verified on.
15+
16+
Presets are used for three kinds of work: finding an optimized baseline, optimizing through patching source code, and supporting new hardware.
17+
18+
**When to use this skill:**
19+
- The user explicitly asks to create a preset, or to optimize model inference via a preset
20+
- Managing already created presets: watching sessions, listing, exporting, and deleting them via `dstack preset` commands
21+
22+
**When NOT to use this skill:**
23+
- Deploying or serving a model: use a service instead (see the `dstack` skill)
24+
25+
## How to use presets
26+
27+
Follow the [presets documentation](https://dstack.ai/docs/concepts/presets.md).
28+
29+
[Configuration reference](https://dstack.ai/docs/reference/dstack.yml/preset.md) | [CLI reference](https://dstack.ai/docs/reference/cli/dstack/preset.md)

src/dstack/_internal/cli/models/preset_agent.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ class PresetSessionState(CoreModel):
118118
trials_num: Optional[int]
119119
previous: list[str]
120120
created_at: datetime
121-
debug: bool
122121
status: PresetSessionStatus
123122
# None is a detached session.
124123
owner: Optional[PresetSessionProcess]

src/dstack/_internal/cli/services/configurators/preset.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def apply_configuration(
6666
configuration=conf,
6767
store=store,
6868
keep_service=configurator_args.keep_service,
69-
debug=configurator_args.debug,
7069
user_prompt=user_prompt,
7170
allowed_fleets=allowed_fleets,
7271
previous=previous,
@@ -131,11 +130,6 @@ def register_creation_args(parser: ArgsParser) -> None:
131130
metavar="N",
132131
help="The number of benchmarked trials before the best one is promoted",
133132
)
134-
parser.add_argument(
135-
"--debug",
136-
action="store_true",
137-
help="Save the agent prompt and raw trace",
138-
)
139133
parser.add_argument(
140134
"--previous",
141135
action="append",

src/dstack/_internal/cli/services/presets/agent.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def _prepare_subprocess_command(command: list[str]) -> list[str]:
375375
return [comspec, "/d", "/s", "/c", subprocess.list2cmdline(command)]
376376

377377

378-
def _write_debug_trace(
378+
def _write_trace(
379379
session: PresetSession,
380380
*,
381381
stream_name: Literal["stdout", "stderr"],
@@ -523,7 +523,7 @@ async def _read_process_stream(
523523
redacted_values: Sequence[str],
524524
session: PresetSession,
525525
) -> PresetAgentProcessOutput:
526-
# stderr feeds the debug trace and advances the persisted offset, but only
526+
# stderr feeds the trace and advances the persisted offset, but only
527527
# stdout can carry the report.
528528
parse_result = stream_name == "stdout"
529529
output = PresetAgentProcessOutput()
@@ -532,13 +532,12 @@ async def _read_process_stream(
532532
if not line:
533533
return output
534534
text = line.decode(errors="replace")
535-
if session.debug:
536-
_write_debug_trace(
537-
session,
538-
stream_name=stream_name,
539-
text=text,
540-
redacted_values=redacted_values,
541-
)
535+
_write_trace(
536+
session,
537+
stream_name=stream_name,
538+
text=text,
539+
redacted_values=redacted_values,
540+
)
542541
if not parse_result:
543542
continue
544543
try:

src/dstack/_internal/cli/services/presets/create.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,6 @@ def create_preset(
400400
store: PresetStore,
401401
keep_service: bool = False,
402402
build_name: Optional[str] = None,
403-
debug: bool = False,
404403
resume_session: Optional[PresetSession] = None,
405404
user_prompt: Optional[str] = None,
406405
allowed_fleets: Optional[tuple[str, ...]] = None,
@@ -409,7 +408,6 @@ def create_preset(
409408
session = resume_session or create_preset_session(
410409
configuration,
411410
previous=tuple(session.preset_id for session in previous),
412-
debug=debug,
413411
)
414412
try:
415413
resolved_configuration = _resolve_preset_env(configuration)
@@ -629,10 +627,9 @@ async def _create_preset(
629627
# A second, persistent copy: the workspace above is deleted with the run,
630628
# while the listing and `--previous` read constraints from the session dir.
631629
session.write_constraints(constraints_text)
632-
if session.debug:
633-
session.write_prompt(prompt)
634-
if setup.auth is not None:
635-
session.write_agent_info(setup.auth)
630+
session.write_prompt(prompt)
631+
if setup.auth is not None:
632+
session.write_agent_info(setup.auth)
636633
try:
637634
if mode == "attach":
638635
process_output = await attach_preset_agent(
@@ -685,12 +682,11 @@ async def _create_preset(
685682
interrupted = True
686683
raise
687684
finally:
688-
if session.debug:
689-
_save_final_report_copy(
690-
workspace=setup.workspace,
691-
session=session,
692-
redacted_values=redacted_values,
693-
)
685+
_save_final_report_copy(
686+
workspace=setup.workspace,
687+
session=session,
688+
redacted_values=redacted_values,
689+
)
694690
if not interrupted:
695691
keep_final_service = keep_service and creation_succeeded
696692
try:
@@ -1009,8 +1005,7 @@ async def _cleanup_runs(
10091005
pending.remove(name)
10101006
if pending:
10111007
await asyncio.sleep(2)
1012-
if session.debug:
1013-
print_preset_progress("All preset creation runs stopped.", session=session)
1008+
print_preset_progress("All preset creation runs stopped.", session=session)
10141009

10151010

10161011
def _load_submitted_run_names(path: Path) -> list[str]:

src/dstack/_internal/cli/services/presets/session.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class SessionBusyError(CLIError):
5656
@dataclass
5757
class PresetSession:
5858
path: Path
59-
debug: bool
6059
preset_id: str
6160
# Background reconcile sets this False so finalizing a detached session stays
6261
# silent on the read command; agent.log is written regardless.
@@ -269,7 +268,6 @@ def create_preset_session(
269268
configuration: PresetConfiguration,
270269
*,
271270
previous: Sequence[str],
272-
debug: bool,
273271
) -> PresetSession:
274272
if configuration.name is None:
275273
raise CLIError("The service name is required to save agent output")
@@ -286,7 +284,8 @@ def create_preset_session(
286284
continue
287285
break
288286
_write_private_text(path / "agent.log", "")
289-
session = PresetSession(path=path, debug=debug, preset_id=preset_id)
287+
_write_private_text(path / "trace.jsonl", "")
288+
session = PresetSession(path=path, preset_id=preset_id)
290289
session.write_state(
291290
PresetSessionState(
292291
id=preset_id,
@@ -295,7 +294,6 @@ def create_preset_session(
295294
trials_num=configuration.trials,
296295
previous=list(previous),
297296
created_at=datetime.now(timezone.utc),
298-
debug=debug,
299297
status="running",
300298
owner=_current_process(),
301299
run=None,
@@ -311,8 +309,6 @@ def create_preset_session(
311309
path / "preset.dstack.yml",
312310
yaml.safe_dump(record, sort_keys=False),
313311
)
314-
if debug:
315-
_write_private_text(path / "trace.jsonl", "")
316312
except OSError as e:
317313
if path is not None:
318314
shutil.rmtree(path, ignore_errors=True)
@@ -322,7 +318,7 @@ def create_preset_session(
322318

323319
def load_resumable_session(preset_id: str) -> PresetSession:
324320
path = get_presets_dir() / preset_id
325-
session = PresetSession(path=path, debug=False, preset_id=preset_id)
321+
session = PresetSession(path=path, preset_id=preset_id)
326322
state = session.read_state()
327323
if not path.is_dir() or state is None:
328324
raise CLIError(f"Unknown preset: {preset_id}")
@@ -337,7 +333,6 @@ def load_resumable_session(preset_id: str) -> PresetSession:
337333
)
338334
if state.run is None or state.run.claude_session_id is None:
339335
raise CLIError(f"Preset {preset_id} creation stopped before it started; create a new one")
340-
session.debug = state.debug
341336
return session
342337

343338

@@ -368,7 +363,7 @@ def session_process_alive(state: PresetSessionState) -> bool:
368363

369364
def load_attachable_session(preset_id: str) -> PresetSession:
370365
path = get_presets_dir() / preset_id
371-
session = PresetSession(path=path, debug=False, preset_id=preset_id)
366+
session = PresetSession(path=path, preset_id=preset_id)
372367
state = session.read_state()
373368
if not path.is_dir() or state is None:
374369
raise CLIError(f"Unknown preset: {preset_id}")
@@ -387,13 +382,12 @@ def load_attachable_session(preset_id: str) -> PresetSession:
387382
f"Preset {preset_id} is already being followed by another CLI (pid {owner.pid});"
388383
f" stop or detach it there with Ctrl+C"
389384
)
390-
session.debug = state.debug
391385
return session
392386

393387

394388
def load_preset_session(preset_id: str) -> PresetSession:
395389
path = get_presets_dir() / preset_id
396-
session = PresetSession(path=path, debug=False, preset_id=preset_id)
390+
session = PresetSession(path=path, preset_id=preset_id)
397391
if not path.is_dir() or session.read_state() is None:
398392
raise CLIError(f"Unknown preset: {preset_id}")
399393
return session
@@ -476,7 +470,7 @@ def iter_preset_sessions() -> Iterator[PresetSession]:
476470
return
477471
for path in sorted(root.iterdir()):
478472
if path.is_dir() and not path.name.startswith((".", "models--")):
479-
yield PresetSession(path=path, debug=False, preset_id=path.name)
473+
yield PresetSession(path=path, preset_id=path.name)
480474

481475

482476
def find_session_name_claims(name: str) -> list[PresetSession]:

src/tests/_internal/cli/commands/test_preset.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,6 @@ def test_merges_profile_configuration_and_cli_args(self, tmp_path):
485485
"0.75",
486486
"--fleet",
487487
"cli-fleet",
488-
"--debug",
489488
],
490489
home_dir=tmp_path,
491490
repo_dir=tmp_path,
@@ -499,7 +498,6 @@ def test_merges_profile_configuration_and_cli_args(self, tmp_path):
499498
assert configuration.max_price == 0.75
500499
assert configuration.spot_policy.value == "spot"
501500
assert [fleet.format() for fleet in configuration.fleets] == ["cli-fleet"]
502-
assert create.call_args.kwargs["debug"] is True
503501

504502
def test_create_detaches_the_name_from_the_old_preset(self, tmp_path):
505503
preset = get_preset().model_copy(update={"name": "qwen"})
@@ -630,7 +628,6 @@ def test_accepts_creation_and_profile_arguments(self, tmp_path):
630628
"7",
631629
"--backend",
632630
"gcp",
633-
"--debug",
634631
],
635632
home_dir=tmp_path,
636633
repo_dir=tmp_path,
@@ -641,7 +638,6 @@ def test_accepts_creation_and_profile_arguments(self, tmp_path):
641638
assert configuration.name == "cli-name"
642639
assert configuration.trials == 7
643640
assert configuration.backends == ["gcp"]
644-
assert create.call_args.kwargs["debug"] is True
645641

646642
def test_rejects_detach(self, tmp_path, capsys):
647643
configuration_path = self._write_configuration(tmp_path)

src/tests/_internal/cli/common.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ def get_session_state(**overrides: Any) -> PresetSessionState:
210210
"trials_num": None,
211211
"previous": [],
212212
"created_at": datetime(2026, 1, 2, 3, 4, tzinfo=timezone.utc),
213-
"debug": False,
214213
"status": "running",
215214
"owner": None,
216215
"run": None,

0 commit comments

Comments
 (0)