@hraness/dawg 0.6.0 → 0.7.0

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.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Command mode (show-me): the agent acts by writing dawg prompt commands, one
3
+ * per line, the way a human types them. Each line runs through the host's
4
+ * command path (the same parser and commit a typed Enter uses) the moment
5
+ * it is complete, while the model is still streaming the next one, so the
6
+ * score changes during the turn. The partial line is reported as it grows
7
+ * (`command-typing`) for the prompt bar's ghost text. A small JSON tool set
8
+ * stays for what commands cannot express: workspace files, the web, media
9
+ * and measuring. Measured against the JSON tool loop on the same tasks this
10
+ * sends about a ninth of the input tokens and finishes in about half the
11
+ * time (docs/show-me.md).
12
+ */
13
+ import { HELP_SECTIONS } from "../commands/help.ts";
14
+ import { MEDIA_TOOLS } from "../media/tools.ts";
15
+ import {
16
+ AGENT_LIMITS,
17
+ MEDIA_PROMPT,
18
+ WORKSPACE_PROMPT,
19
+ classifyAgentError,
20
+ executeCall,
21
+ hostProjectOutline,
22
+ tighten,
23
+ turnDeadline,
24
+ type AgentEvent,
25
+ type AgentTurnOptions,
26
+ type AgentTurnResult,
27
+ } from "./agent.ts";
28
+ import { compositionBrief } from "./brief.ts";
29
+ import type { ChatMessage, ChatToolCall } from "./gateway.ts";
30
+ import { CommandLines } from "./show-me.ts";
31
+ import { AGENT_TOOLS, chatTools } from "./tools.ts";
32
+ import { SseBudgetError } from "./sse.ts";
33
+
34
+ /** What a host does with one complete command line. */
35
+ export type CommandOutcome = Readonly<{
36
+ ok: boolean;
37
+ /** The receipt a human would see (`tempo · 96`) or the error. */
38
+ message: string;
39
+ baseRevision: number;
40
+ resultRevision: number;
41
+ }>;
42
+
43
+ export type CommandHost = Readonly<{
44
+ /** Whether a complete line is a song-editing command (else it is prose). */
45
+ isCommand(line: string): boolean;
46
+ /** Run a line exactly as if the user had typed it and pressed Enter. */
47
+ run(line: string): Promise<CommandOutcome>;
48
+ }>;
49
+
50
+ /** The tools a command-mode model still gets: what commands cannot say. */
51
+ export const COMMAND_MODE_TOOL_NAMES: ReadonlySet<string> = new Set([
52
+ "list_files",
53
+ "read_file",
54
+ "write_file",
55
+ "edit_file",
56
+ "web_search",
57
+ "fetch_url",
58
+ "explain",
59
+ "preview_sound",
60
+ "measure_mix",
61
+ ...MEDIA_TOOLS.map((tool) => tool.name),
62
+ ]);
63
+
64
+ /** Steps per command-mode turn: each extra one is a correction round. */
65
+ export const COMMAND_MAX_STEPS = 4;
66
+
67
+ function commandReference(): string {
68
+ // Song commands only: the window and agent groups change the person's
69
+ // window, which the agent never does.
70
+ return HELP_SECTIONS.filter(
71
+ (section) =>
72
+ section.group !== "project · window" && section.group !== "agent",
73
+ )
74
+ .flatMap((section) =>
75
+ section.entries.map((entry) => `${entry.command} — ${entry.summary}`),
76
+ )
77
+ .join("\n");
78
+ }
79
+
80
+ export const COMMAND_AGENT_PROMPT = [
81
+ "You are dawg, a loop composer inside a terminal music workstation, and you teach it by doing.",
82
+ "Act by writing dawg prompt commands, exactly as a human types them in the prompt bar, one per line. The user watches each command appear in their prompt bar as you write it, and each line runs the moment it ends, so they learn the commands by watching you.",
83
+ "Prefer the gestures a person would use: a parameter as one value command (fx reverb mix 0.4, volume 0.7, pan -0.3, which the user sees as a fader moving), notes one per line with add (add C4 at 0 for 0.5, played on the user's keyboard as you write them), drums with hit, pattern and euclid on a kit track, a whole groove with groove <name>.",
84
+ "Commands apply to the focused track. /track <name> focuses a track or creates it; instrument kit makes a drum track. Times are beats from 0: in 4/4 musicians' beats 2 and 4 are beats 1 and 3 here, and bar n starts at (n-1)×beats per bar.",
85
+ "Write commands first, with no numbering, quotes or code fences. Then write one short plain sentence (not a command) that says what changed and names the key command so the user can do it by hand next time.",
86
+ "If a command fails you get its error; correct it with another command or stop.",
87
+ "Use the tools only for what commands cannot do: project files, the web, media, previewing and measuring.",
88
+ WORKSPACE_PROMPT,
89
+ MEDIA_PROMPT,
90
+ ].join(" ");
91
+
92
+ /** The system prompt with the command reference, built once. */
93
+ let systemPrompt: string | undefined;
94
+ export function commandAgentSystemPrompt(): string {
95
+ systemPrompt ??= `${COMMAND_AGENT_PROMPT}\n\nCommands:\n${commandReference()}\n/track <name> — focus or create a track`;
96
+ return systemPrompt;
97
+ }
98
+
99
+ export type CommandAgentTurnOptions = AgentTurnOptions &
100
+ Readonly<{ commands: CommandHost }>;
101
+
102
+ /**
103
+ * One command-mode turn. Lines run in order on one chain, so a slow command
104
+ * never reorders the next, and reading the stream never waits on them.
105
+ */
106
+ export async function runCommandAgentTurn(
107
+ options: CommandAgentTurnOptions,
108
+ ): Promise<AgentTurnResult> {
109
+ const limits = {
110
+ maxSteps: Math.min(
111
+ COMMAND_MAX_STEPS,
112
+ tighten(options.budget?.maxSteps, AGENT_LIMITS.maxSteps),
113
+ ),
114
+ maxToolCalls: tighten(
115
+ options.budget?.maxToolCalls,
116
+ AGENT_LIMITS.maxToolCalls,
117
+ ),
118
+ maxResponseBytes: tighten(
119
+ options.budget?.maxResponseBytes,
120
+ AGENT_LIMITS.maxResponseBytes,
121
+ ),
122
+ timeoutMs: tighten(options.budget?.timeoutMs, AGENT_LIMITS.timeoutMs),
123
+ };
124
+ const emit = (event: AgentEvent) => {
125
+ try {
126
+ options.onEvent?.(event);
127
+ } catch {
128
+ // A rendering failure must never corrupt the turn.
129
+ }
130
+ };
131
+ const allTools = options.tools ?? AGENT_TOOLS;
132
+ const offered = chatTools(
133
+ allTools.filter((tool) => COMMAND_MODE_TOOL_NAMES.has(tool.name)),
134
+ );
135
+ const nonce = Math.random().toString(36).slice(2, 6);
136
+ const newNoteId =
137
+ options.newNoteId ??
138
+ ((trackId: string, revision: number, index: number) =>
139
+ `${trackId.slice(0, 40)}-${revision}-${nonce}${index}`);
140
+ const deadline = turnDeadline(limits.timeoutMs);
141
+ const signal = options.signal
142
+ ? AbortSignal.any([options.signal, deadline.signal])
143
+ : deadline.signal;
144
+ const currentRevision = () => options.host.snapshot().revision;
145
+ const turnStartRevision = currentRevision();
146
+
147
+ let applied = 0;
148
+ let rejected = 0;
149
+ let toolCalls = 0;
150
+ let bytesUsed = 0;
151
+ let finalText = "";
152
+ const messages: ChatMessage[] = [
153
+ { role: "user", content: options.prompt.slice(0, 8_000) },
154
+ ];
155
+ const finish = (result: AgentTurnResult): AgentTurnResult => {
156
+ deadline.clear();
157
+ emit(result);
158
+ return result;
159
+ };
160
+ const done = (reason: "stop" | "max-steps" | "max-tool-calls") =>
161
+ finish({
162
+ type: "done",
163
+ reason,
164
+ text: finalText,
165
+ applied,
166
+ rejected,
167
+ revision: currentRevision(),
168
+ });
169
+
170
+ try {
171
+ for (let step = 1; step <= limits.maxSteps; step += 1) {
172
+ if (signal.aborted) throw signal.reason;
173
+ for (const steer of options.host.takeSteering?.() ?? [])
174
+ messages.push({
175
+ role: "user",
176
+ content: `Steering from the user mid-turn: ${steer.slice(0, 2_000)}`,
177
+ });
178
+ emit({ type: "step", step });
179
+ const snapshot = options.host.snapshot();
180
+ const brief = compositionBrief({
181
+ ...snapshot,
182
+ project: await hostProjectOutline(options.host, snapshot),
183
+ });
184
+ const briefText =
185
+ snapshot.revision === turnStartRevision
186
+ ? `Composition brief (JSON): ${brief}`
187
+ : `Composition brief (JSON, current score at revision ${snapshot.revision}: it already includes every command you ran this turn; do not repeat them): ${brief}`;
188
+ const remaining = limits.maxResponseBytes - bytesUsed;
189
+ if (remaining <= 0) throw new SseBudgetError(limits.maxResponseBytes);
190
+
191
+ const lines = new CommandLines();
192
+ let text = "";
193
+ const prose: string[] = [];
194
+ const results: string[] = [];
195
+ let failed = 0;
196
+ let chain: Promise<void> = Promise.resolve();
197
+ const runLine = (line: string) => {
198
+ if (!options.commands.isCommand(line)) {
199
+ prose.push(line);
200
+ return;
201
+ }
202
+ if (toolCalls >= limits.maxToolCalls) return;
203
+ toolCalls += 1;
204
+ const callId = `cmd-${step}-${toolCalls}`;
205
+ chain = chain.then(async () => {
206
+ if (signal.aborted) return;
207
+ emit({ type: "tool-start", callId, name: "command", step });
208
+ let outcome: CommandOutcome;
209
+ try {
210
+ outcome = await options.commands.run(line);
211
+ } catch (error) {
212
+ outcome = {
213
+ ok: false,
214
+ message: error instanceof Error ? error.message : String(error),
215
+ baseRevision: currentRevision(),
216
+ resultRevision: currentRevision(),
217
+ };
218
+ }
219
+ emit({ type: "command", line, ...outcome });
220
+ if (outcome.ok) {
221
+ if (outcome.resultRevision !== outcome.baseRevision) applied += 1;
222
+ results.push(`${line} → ${outcome.message}`);
223
+ } else {
224
+ failed += 1;
225
+ rejected += 1;
226
+ results.push(`${line} → FAILED: ${outcome.message}`);
227
+ }
228
+ });
229
+ };
230
+ const pending = new Map<
231
+ number,
232
+ { id: string; name: string; arguments: string }
233
+ >();
234
+ let typing = "";
235
+ for await (const event of options.client.stream(
236
+ {
237
+ model: options.model,
238
+ ...(options.fallbackModel
239
+ ? { fallbackModelId: options.fallbackModel }
240
+ : {}),
241
+ messages: [
242
+ { role: "system", content: commandAgentSystemPrompt() },
243
+ { role: "system", content: briefText },
244
+ ...messages,
245
+ ],
246
+ ...(offered.length > 0 ? { tools: offered } : {}),
247
+ maxResponseBytes: remaining,
248
+ },
249
+ signal,
250
+ )) {
251
+ if (event.type === "usage") {
252
+ const { type: _type, ...usage } = event;
253
+ emit({ type: "usage", ...usage });
254
+ } else if (event.type === "activity") {
255
+ emit({ type: "activity", message: event.message });
256
+ } else if (event.type === "text") {
257
+ bytesUsed += event.delta.length;
258
+ if (text.length >= AGENT_LIMITS.maxTextChars) continue;
259
+ text += event.delta;
260
+ const { complete, partial } = lines.push(event.delta);
261
+ for (const line of complete) runLine(line);
262
+ if (partial !== typing) {
263
+ typing = partial;
264
+ emit({ type: "command-typing", text: partial });
265
+ }
266
+ } else if (event.type === "tool-delta") {
267
+ const call = pending.get(event.index) ?? {
268
+ id: "",
269
+ name: "",
270
+ arguments: "",
271
+ };
272
+ if (event.id) call.id = event.id;
273
+ if (event.name) call.name += event.name;
274
+ if (event.arguments) {
275
+ call.arguments += event.arguments;
276
+ bytesUsed += event.arguments.length;
277
+ if (call.arguments.length > AGENT_LIMITS.maxToolArgumentBytes)
278
+ throw new SseBudgetError(AGENT_LIMITS.maxToolArgumentBytes);
279
+ }
280
+ pending.set(event.index, call);
281
+ }
282
+ }
283
+ const last = lines.flush();
284
+ if (last) runLine(last);
285
+ if (typing) emit({ type: "command-typing", text: "" });
286
+ await chain;
287
+ if (signal.aborted) throw signal.reason;
288
+ if (prose.length > 0) {
289
+ finalText = prose.join(" ");
290
+ emit({ type: "text-delta", delta: finalText });
291
+ }
292
+
293
+ const calls = [...pending.entries()]
294
+ .sort(([left], [right]) => left - right)
295
+ .map(([index, call]) => ({
296
+ ...call,
297
+ id: call.id || `call-${step}-${index}`,
298
+ }));
299
+ if (calls.length === 0 && failed === 0) return done("stop");
300
+ if (calls.length > 0) {
301
+ const assistantCalls: ChatToolCall[] = calls.map((call) => ({
302
+ id: call.id,
303
+ type: "function",
304
+ function: { name: call.name, arguments: call.arguments || "{}" },
305
+ }));
306
+ messages.push({
307
+ role: "assistant",
308
+ content: text || null,
309
+ tool_calls: assistantCalls,
310
+ });
311
+ for (const call of calls) {
312
+ if (signal.aborted) throw signal.reason;
313
+ toolCalls += 1;
314
+ emit({ type: "tool-start", callId: call.id, name: call.name, step });
315
+ const outcome = await executeCall(call, {
316
+ tools: allTools,
317
+ host: options.host,
318
+ newNoteId,
319
+ signal,
320
+ suspendTimeout: deadline.suspend,
321
+ onProgress: (line) =>
322
+ emit({
323
+ type: "tool-progress",
324
+ callId: call.id,
325
+ name: call.name,
326
+ line,
327
+ }),
328
+ });
329
+ if (outcome.ok) {
330
+ applied += outcome.mutated ? 1 : 0;
331
+ if (outcome.explanation) finalText = outcome.explanation;
332
+ emit(outcome.event);
333
+ } else {
334
+ rejected += 1;
335
+ emit({
336
+ type: "tool-rejected",
337
+ callId: call.id,
338
+ name: call.name,
339
+ diagnostic: outcome.diagnostic,
340
+ });
341
+ }
342
+ messages.push({
343
+ role: "tool",
344
+ tool_call_id: call.id,
345
+ content: outcome.content,
346
+ });
347
+ }
348
+ } else messages.push({ role: "assistant", content: text });
349
+ if (results.length > 0)
350
+ messages.push({
351
+ role: "user",
352
+ content: `Command results:\n${results.join("\n").slice(0, 6_000)}`,
353
+ });
354
+ if (toolCalls >= limits.maxToolCalls) return done("max-tool-calls");
355
+ }
356
+ return done("max-steps");
357
+ } catch (error) {
358
+ const { code, message } = classifyAgentError(error, signal, options.signal);
359
+ return finish({
360
+ type: "error",
361
+ code,
362
+ message,
363
+ applied,
364
+ revision: currentRevision(),
365
+ });
366
+ } finally {
367
+ deadline.clear();
368
+ }
369
+ }
@@ -38,7 +38,7 @@ export const DRUM_TOOLS: readonly AgentTool[] = Object.freeze([
38
38
  {
39
39
  name: "list_drum_patterns",
40
40
  description:
41
- "List the built-in drum patterns (name · tempo range · genre tags · voices). Filter by a word such as house, trap or 2-step.",
41
+ "List the built-in grooves (name · tempo range · style tags · drums). Filter by a word such as house, trap or 2-step.",
42
42
  parameters: {
43
43
  type: "object",
44
44
  properties: { filter: { type: "string", maxLength: 40 } },
@@ -105,7 +105,7 @@ export const DRUM_TOOLS: readonly AgentTool[] = Object.freeze([
105
105
  },
106
106
  {
107
107
  name: "set_drum_kit",
108
- description: `Choose the synthesized drum kit of a drum track: ${SYNTH_KITS.map((kit) => kit.name).join(", ")}, or default. Offline and instant; sample kits (909, 808, linn, …) go through use_sound.`,
108
+ description: `Choose the synthesized kit of a drum track: ${SYNTH_KITS.map((kit) => kit.name).join(", ")}, or default. Offline and instant; sample kits (909, 808, linn, …) go through use_sound.`,
109
109
  parameters: {
110
110
  type: "object",
111
111
  properties: {
@@ -9,6 +9,7 @@ import {
9
9
  ExpressionValidationError,
10
10
  GLIDE_MODES,
11
11
  PEDAL_STATES,
12
+ SOSTENUTO_STATES,
12
13
  VELOCITY_CURVES,
13
14
  normalizeArticulation,
14
15
  normalizeBend,
@@ -20,12 +21,14 @@ import {
20
21
  normalizeVibrato,
21
22
  normalizeNoteHumanize,
22
23
  type NoteExpressionPatch,
24
+ type PedalState,
23
25
  } from "../../core/expression.ts";
24
26
  import {
25
27
  SCORE_LIMITS,
26
28
  type ScoreOperation,
27
29
  type TrackPatch,
28
30
  } from "../../core/score.ts";
31
+ import { softPedalNote } from "../../core/keys.ts";
29
32
  import { loopTicksOf } from "../../core/tempo.ts";
30
33
  import { BEND_SHAPES, barPedal } from "../commands/expression.ts";
31
34
  import type { AgentTool, ToolContext } from "./tools.ts";
@@ -333,4 +336,92 @@ export const EXPRESSION_TOOLS: readonly AgentTool[] = Object.freeze([
333
336
  };
334
337
  },
335
338
  },
339
+ {
340
+ // keys-electric (0.6.1): the modelled piano's soft and middle pedals.
341
+ name: "set_piano_pedals",
342
+ description:
343
+ 'Modeled piano soft (una corda: fewer strings, softer, darker) and sostenuto (holds only keys down when it presses) pedals. Each: [{beat, state}], "bars" (whole loop) or null. Sostenuto has no half.',
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ trackId: { type: "string" },
348
+ soft: pedalSchema(PEDAL_STATES),
349
+ sostenuto: pedalSchema(SOSTENUTO_STATES),
350
+ },
351
+ additionalProperties: false,
352
+ },
353
+ plan(args, context) {
354
+ const trackId = trackOf(args, context);
355
+ const score = context.score;
356
+ const patch: Record<string, unknown> = {};
357
+ if (args.soft !== undefined)
358
+ patch.softPedal = pedalLane(args.soft, "soft", score, PEDAL_STATES);
359
+ if (args.sostenuto !== undefined)
360
+ patch.sostenuto = pedalLane(
361
+ args.sostenuto,
362
+ "sostenuto",
363
+ score,
364
+ SOSTENUTO_STATES,
365
+ );
366
+ if (Object.keys(patch).length === 0)
367
+ throw new ExpressionToolError("set soft, sostenuto or both");
368
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
369
+ const silent =
370
+ patch.softPedal && track ? softPedalNote(track) : undefined;
371
+ if (silent) throw new ExpressionToolError(`soft pedal · ${silent}`);
372
+ return {
373
+ kind: "score",
374
+ operations: [
375
+ { type: "updateTrack", trackId, patch: patch as TrackPatch },
376
+ ],
377
+ trackId,
378
+ summary: `${trackId} ${Object.entries(patch)
379
+ .map(([key, value]) => (value === null ? `${key} off` : key))
380
+ .join(", ")}`,
381
+ };
382
+ },
383
+ },
336
384
  ] satisfies AgentTool[]);
385
+
386
+ function pedalSchema(states: readonly string[]) {
387
+ return {
388
+ type: ["array", "string", "null"],
389
+ items: {
390
+ type: "object",
391
+ properties: {
392
+ beat: { type: "number" },
393
+ state: { type: "string", enum: states },
394
+ },
395
+ },
396
+ };
397
+ }
398
+
399
+ /** A soft or sostenuto lane from tool arguments: events, "bars" or null. */
400
+ function pedalLane(
401
+ value: unknown,
402
+ label: string,
403
+ score: ToolContext["score"],
404
+ states: readonly PedalState[],
405
+ ): unknown {
406
+ if (value === null) return null;
407
+ // Soft and sostenuto hold through the loop (no re-pedal at each bar).
408
+ if (value === "bars")
409
+ return [
410
+ { tick: 0, state: "down" },
411
+ { tick: loopTicksOf(score), state: "up" },
412
+ ];
413
+ if (!Array.isArray(value))
414
+ throw new ExpressionToolError(`${label} must be an array, "bars" or null`);
415
+ const events = value.map((item: unknown, index) => {
416
+ const event = (item ?? {}) as Record<string, unknown>;
417
+ const at = beat(event, "beat");
418
+ if (at === undefined)
419
+ throw new ExpressionToolError(`${label}[${index}].beat is required`);
420
+ return { tick: Math.round(at * score.ticksPerBeat), state: event.state };
421
+ });
422
+ return (
423
+ validated(() =>
424
+ normalizePedal(events, loopTicksOf(score), label, states),
425
+ ) ?? null
426
+ );
427
+ }