@hraness/dawg 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +292 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { compositionBrief } from "./brief.ts";
|
|
2
|
+
import {
|
|
3
|
+
AGENT_LIMITS,
|
|
4
|
+
MEDIA_PROMPT,
|
|
5
|
+
classifyAgentError,
|
|
6
|
+
executeCall,
|
|
7
|
+
hostProjectOutline,
|
|
8
|
+
tighten,
|
|
9
|
+
turnDeadline,
|
|
10
|
+
WORKSPACE_PROMPT,
|
|
11
|
+
type AgentBudget,
|
|
12
|
+
type AgentEvent,
|
|
13
|
+
type AgentHost,
|
|
14
|
+
type AgentTurnResult,
|
|
15
|
+
} from "./agent.ts";
|
|
16
|
+
import { AGENT_TOOLS, type AgentTool } from "./tools.ts";
|
|
17
|
+
import { parseTextReply, XcbError, type TextReply } from "./xcb.ts";
|
|
18
|
+
import { SseBudgetError } from "./sse.ts";
|
|
19
|
+
|
|
20
|
+
/** Steps per xcb turn. Each step is one full model call, so keep it small. */
|
|
21
|
+
export const XCB_MAX_STEPS = 3;
|
|
22
|
+
const MAX_PROMPT_CHARS = 8_000;
|
|
23
|
+
const MAX_FEEDBACK_CHARS = 6_000;
|
|
24
|
+
|
|
25
|
+
/** One text-in/text-out model call; `generate` must honour `signal`. */
|
|
26
|
+
export type TextGenerate = (
|
|
27
|
+
prompt: string,
|
|
28
|
+
options: { signal: AbortSignal; timeoutMs: number; maxOutputBytes: number },
|
|
29
|
+
) => Promise<string>;
|
|
30
|
+
|
|
31
|
+
export type TextAgentTurnOptions = Readonly<{
|
|
32
|
+
prompt: string;
|
|
33
|
+
generate: TextGenerate;
|
|
34
|
+
host: AgentHost;
|
|
35
|
+
onEvent?: (event: AgentEvent) => void;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
budget?: AgentBudget;
|
|
38
|
+
tools?: readonly AgentTool[];
|
|
39
|
+
newNoteId?: (trackId: string, revision: number, index: number) => string;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export const TEXT_AGENT_SYSTEM_PROMPT = [
|
|
43
|
+
"You are dawg, a loop composer inside a terminal music workstation.",
|
|
44
|
+
"You edit the score by returning operations. Each op is validated and applied immediately, in order, as its own revision.",
|
|
45
|
+
"Times are in beats from the loop start (0-based). Keep notes inside loopBeats unless you extend the loop first.",
|
|
46
|
+
"Prefer a few well-formed ops (one add_notes op per track part) over many tiny ones.",
|
|
47
|
+
'For drums, create a track with instrument "kit" and use add_drums; drum pitches select voices, so do not use add_notes for beats.',
|
|
48
|
+
"Effects (set_effects, set_automation): low-pass filter cutoff 20..20000 Hz and resonance 0..1; stereo delay beats 0.0625..4, feedback 0..0.9, mix 0..1; stereo reverb mix 0..1 (0.15..0.35 is a natural room) and size 0..1; pan -1..1 is equal-power stereo. Automatable lanes: volume, pan, filter, resonance, delay-feedback, delay-mix.",
|
|
49
|
+
'Reply with exactly one JSON object and nothing else, shaped {"ops":[{"tool":"<tool name>","args":{...}}],"say":"<one short sentence describing the musical change>","done":true}.',
|
|
50
|
+
'Set "done":false only if you need to see the results of these ops before continuing; you will then get each op\'s result and can send more ops.',
|
|
51
|
+
"If an op was rejected, read its diagnostic and either send a corrected op or stop with done:true.",
|
|
52
|
+
WORKSPACE_PROMPT,
|
|
53
|
+
MEDIA_PROMPT,
|
|
54
|
+
'Media tools return their outputs in the op result; send them alone with "done":false and chain on the result.',
|
|
55
|
+
].join(" ");
|
|
56
|
+
|
|
57
|
+
/** Render the tool registry as a compact op catalog for a text-only model. */
|
|
58
|
+
export function renderToolCatalog(
|
|
59
|
+
tools: readonly AgentTool[] = AGENT_TOOLS,
|
|
60
|
+
): string {
|
|
61
|
+
return tools
|
|
62
|
+
.map(
|
|
63
|
+
(tool) =>
|
|
64
|
+
`- ${tool.name}: ${tool.description}\n args schema: ${JSON.stringify(tool.parameters)}`,
|
|
65
|
+
)
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A tool loop for providers without tool calling (xcb). The model returns one
|
|
71
|
+
* JSON object of ops; each op goes through the same tool argument checks,
|
|
72
|
+
* operation validator, reducer dry run and per-op commit as the gateway path.
|
|
73
|
+
* Rejected ops or `done:false` trigger another call with diagnostics, up to
|
|
74
|
+
* `XCB_MAX_STEPS` calls and the usual turn budgets.
|
|
75
|
+
*/
|
|
76
|
+
export async function runTextAgentTurn(
|
|
77
|
+
options: TextAgentTurnOptions,
|
|
78
|
+
): Promise<AgentTurnResult> {
|
|
79
|
+
const limits = {
|
|
80
|
+
maxSteps: Math.min(
|
|
81
|
+
XCB_MAX_STEPS,
|
|
82
|
+
tighten(options.budget?.maxSteps, AGENT_LIMITS.maxSteps),
|
|
83
|
+
),
|
|
84
|
+
maxToolCalls: tighten(
|
|
85
|
+
options.budget?.maxToolCalls,
|
|
86
|
+
AGENT_LIMITS.maxToolCalls,
|
|
87
|
+
),
|
|
88
|
+
maxResponseBytes: tighten(
|
|
89
|
+
options.budget?.maxResponseBytes,
|
|
90
|
+
AGENT_LIMITS.maxResponseBytes,
|
|
91
|
+
),
|
|
92
|
+
timeoutMs: tighten(options.budget?.timeoutMs, AGENT_LIMITS.timeoutMs),
|
|
93
|
+
};
|
|
94
|
+
const emit = (event: AgentEvent) => {
|
|
95
|
+
try {
|
|
96
|
+
options.onEvent?.(event);
|
|
97
|
+
} catch {
|
|
98
|
+
// A rendering failure must never corrupt the turn.
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const tools = options.tools ?? AGENT_TOOLS;
|
|
102
|
+
const catalog = renderToolCatalog(tools);
|
|
103
|
+
const nonce = Math.random().toString(36).slice(2, 6);
|
|
104
|
+
const newNoteId =
|
|
105
|
+
options.newNoteId ??
|
|
106
|
+
((trackId: string, revision: number, index: number) =>
|
|
107
|
+
`${trackId.slice(0, 40)}-${revision}-${nonce}${index}`);
|
|
108
|
+
const startedAt = Date.now();
|
|
109
|
+
const deadline = turnDeadline(limits.timeoutMs);
|
|
110
|
+
const signal = options.signal
|
|
111
|
+
? AbortSignal.any([options.signal, deadline.signal])
|
|
112
|
+
: deadline.signal;
|
|
113
|
+
|
|
114
|
+
let applied = 0;
|
|
115
|
+
let rejected = 0;
|
|
116
|
+
let toolCalls = 0;
|
|
117
|
+
let bytesUsed = 0;
|
|
118
|
+
let finalText = "";
|
|
119
|
+
const steering: string[] = [];
|
|
120
|
+
let feedback = "";
|
|
121
|
+
const currentRevision = () => options.host.snapshot().revision;
|
|
122
|
+
const finish = (result: AgentTurnResult): AgentTurnResult => {
|
|
123
|
+
deadline.clear();
|
|
124
|
+
emit(result);
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
const done = (reason: "stop" | "max-steps" | "max-tool-calls") =>
|
|
128
|
+
finish({
|
|
129
|
+
type: "done",
|
|
130
|
+
reason,
|
|
131
|
+
text: finalText,
|
|
132
|
+
applied,
|
|
133
|
+
rejected,
|
|
134
|
+
revision: currentRevision(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
for (let step = 1; step <= limits.maxSteps; step += 1) {
|
|
139
|
+
if (signal.aborted) throw signal.reason;
|
|
140
|
+
for (const steer of options.host.takeSteering?.() ?? [])
|
|
141
|
+
steering.push(steer.slice(0, 2_000));
|
|
142
|
+
emit({ type: "step", step });
|
|
143
|
+
const remainingBytes = limits.maxResponseBytes - bytesUsed;
|
|
144
|
+
if (remainingBytes <= 0)
|
|
145
|
+
throw new SseBudgetError(limits.maxResponseBytes);
|
|
146
|
+
const snapshot = options.host.snapshot();
|
|
147
|
+
const brief = compositionBrief({
|
|
148
|
+
...snapshot,
|
|
149
|
+
project: await hostProjectOutline(options.host, snapshot),
|
|
150
|
+
});
|
|
151
|
+
const prompt = [
|
|
152
|
+
TEXT_AGENT_SYSTEM_PROMPT,
|
|
153
|
+
`Tools:\n${catalog}`,
|
|
154
|
+
`Composition brief (JSON): ${brief}`,
|
|
155
|
+
`User request: ${options.prompt.slice(0, MAX_PROMPT_CHARS)}`,
|
|
156
|
+
...steering.map((text) => `Steering from the user mid-turn: ${text}`),
|
|
157
|
+
feedback
|
|
158
|
+
? `Results of your previous ops (the brief above already reflects accepted ones):\n${feedback}`
|
|
159
|
+
: "",
|
|
160
|
+
"Reply now with the single JSON object.",
|
|
161
|
+
]
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.join("\n\n");
|
|
164
|
+
const remainingMs = limits.timeoutMs - (Date.now() - startedAt);
|
|
165
|
+
const reply = await options.generate(prompt, {
|
|
166
|
+
signal,
|
|
167
|
+
timeoutMs: Math.max(1_000, remainingMs),
|
|
168
|
+
maxOutputBytes: Math.min(remainingBytes, 64 * 1024),
|
|
169
|
+
});
|
|
170
|
+
if (signal.aborted) throw signal.reason;
|
|
171
|
+
bytesUsed += reply.length;
|
|
172
|
+
|
|
173
|
+
let parsed: TextReply;
|
|
174
|
+
try {
|
|
175
|
+
parsed = parseTextReply(reply);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const diagnostic =
|
|
178
|
+
error instanceof XcbError
|
|
179
|
+
? error.message
|
|
180
|
+
: "reply could not be parsed";
|
|
181
|
+
rejected += 1;
|
|
182
|
+
emit({
|
|
183
|
+
type: "tool-rejected",
|
|
184
|
+
callId: `xcb-${step}-reply`,
|
|
185
|
+
name: "reply",
|
|
186
|
+
diagnostic,
|
|
187
|
+
});
|
|
188
|
+
feedback = `Your reply was rejected: ${diagnostic}. Reply with exactly one JSON object.`;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (parsed.say) {
|
|
192
|
+
finalText = parsed.say;
|
|
193
|
+
emit({ type: "text-delta", delta: parsed.say });
|
|
194
|
+
}
|
|
195
|
+
const results: string[] = [];
|
|
196
|
+
let stepRejected = 0;
|
|
197
|
+
let overBudget = false;
|
|
198
|
+
for (const [index, op] of parsed.ops.entries()) {
|
|
199
|
+
if (signal.aborted) throw signal.reason;
|
|
200
|
+
const callId = `xcb-${step}-${index}`;
|
|
201
|
+
if (toolCalls >= limits.maxToolCalls) {
|
|
202
|
+
overBudget = true;
|
|
203
|
+
rejected += 1;
|
|
204
|
+
stepRejected += 1;
|
|
205
|
+
emit({
|
|
206
|
+
type: "tool-rejected",
|
|
207
|
+
callId,
|
|
208
|
+
name: op.tool,
|
|
209
|
+
diagnostic: "tool call budget exhausted",
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
toolCalls += 1;
|
|
214
|
+
emit({ type: "tool-start", callId, name: op.tool, step });
|
|
215
|
+
const outcome = await executeCall(
|
|
216
|
+
{ id: callId, name: op.tool, arguments: JSON.stringify(op.args) },
|
|
217
|
+
{
|
|
218
|
+
tools,
|
|
219
|
+
host: options.host,
|
|
220
|
+
newNoteId,
|
|
221
|
+
signal,
|
|
222
|
+
suspendTimeout: deadline.suspend,
|
|
223
|
+
onProgress: (line) =>
|
|
224
|
+
emit({ type: "tool-progress", callId, name: op.tool, line }),
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
if (outcome.ok) {
|
|
228
|
+
applied += outcome.mutated ? 1 : 0;
|
|
229
|
+
if (outcome.explanation) finalText = outcome.explanation;
|
|
230
|
+
emit(outcome.event);
|
|
231
|
+
} else {
|
|
232
|
+
rejected += 1;
|
|
233
|
+
stepRejected += 1;
|
|
234
|
+
emit({
|
|
235
|
+
type: "tool-rejected",
|
|
236
|
+
callId,
|
|
237
|
+
name: op.tool,
|
|
238
|
+
diagnostic: outcome.diagnostic,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
results.push(`ops[${index}] ${op.tool}: ${outcome.content}`);
|
|
242
|
+
}
|
|
243
|
+
if (overBudget || toolCalls >= limits.maxToolCalls)
|
|
244
|
+
return done("max-tool-calls");
|
|
245
|
+
if (stepRejected === 0 && parsed.done) return done("stop");
|
|
246
|
+
feedback =
|
|
247
|
+
results.join("\n").slice(0, MAX_FEEDBACK_CHARS) || "No ops were sent.";
|
|
248
|
+
}
|
|
249
|
+
return done("max-steps");
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const { code, message } = classifyAgentError(error, signal, options.signal);
|
|
252
|
+
return finish({
|
|
253
|
+
type: "error",
|
|
254
|
+
code,
|
|
255
|
+
message,
|
|
256
|
+
applied,
|
|
257
|
+
revision: currentRevision(),
|
|
258
|
+
});
|
|
259
|
+
} finally {
|
|
260
|
+
deadline.clear();
|
|
261
|
+
}
|
|
262
|
+
}
|