aiden-runtime 4.7.0 → 4.8.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.
@@ -45,19 +45,35 @@ class AuxiliaryClient {
45
45
  return this.opts.adapter;
46
46
  if (!this.opts.resolver)
47
47
  return null;
48
- this.resolveCallCount += 1;
49
- try {
50
- const adapter = await this.opts.resolver.resolve({
51
- providerId: this.opts.defaultProvider,
52
- modelId: this.opts.defaultModel,
53
- });
54
- return adapter;
55
- }
56
- catch (err) {
57
- this.warn(`auxiliary client unavailable (${this.opts.defaultProvider}/${this.opts.defaultModel}): ${err.message}`);
58
- this.adapterUnavailable = true;
59
- return null;
48
+ // v4.8.0 Slice 11 — resolution chain: default first, then each
49
+ // fallback in order. The first attempt that resolves wins. This
50
+ // is the routing-fix entry point for the chatgpt-plus + gpt-5
51
+ // bug: aidenCLI hands us Groq as the default and the parent
52
+ // provider/model as the fallback, so auxiliary calls land on
53
+ // Groq when configured and the parent only sees traffic when
54
+ // Groq is absent.
55
+ const attempts = [
56
+ { providerId: this.opts.defaultProvider, modelId: this.opts.defaultModel },
57
+ ...(this.opts.fallbacks ?? []),
58
+ ];
59
+ const failures = [];
60
+ for (const att of attempts) {
61
+ this.resolveCallCount += 1;
62
+ try {
63
+ const adapter = await this.opts.resolver.resolve({
64
+ providerId: att.providerId,
65
+ modelId: att.modelId,
66
+ });
67
+ this.warn(`auxiliary resolved via ${att.providerId}/${att.modelId}`);
68
+ return adapter;
69
+ }
70
+ catch (err) {
71
+ failures.push(`${att.providerId}/${att.modelId}: ${err.message}`);
72
+ }
60
73
  }
74
+ this.warn(`auxiliary client unavailable (tried ${attempts.length}): ${failures.join('; ')}`);
75
+ this.adapterUnavailable = true;
76
+ return null;
61
77
  }
62
78
  /** Resolve count for tests (verifies single-resolution behaviour). */
63
79
  _resolveCallCount() {
@@ -122,7 +138,24 @@ class AuxiliaryClient {
122
138
  this.usage.set(purpose, cur);
123
139
  }
124
140
  warn(msg) {
125
- (this.opts.warn ?? ((m) => console.warn(`[auxiliary] ${m}`)))(msg);
141
+ // v4.8.0 Slice 5 gate console output behind AIDEN_VERBOSE.
142
+ // Auxiliary failures are recoverable (the main loop continues;
143
+ // result content is just empty), so the warning is pure noise
144
+ // for end users. Power users set AIDEN_VERBOSE=1 to surface them.
145
+ // Inline env-read preserves the core → cli no-import invariant;
146
+ // canonical isVerbose() lives at cli/v4/design/tokens.ts.
147
+ //
148
+ // v4.8.0 Slice 11 — if opts.warn is explicitly injected, always
149
+ // forward (tests + advanced callers register their own sink and
150
+ // expect every message). The AIDEN_VERBOSE gate now applies only
151
+ // to the default console.warn fallback that end-users see.
152
+ if (this.opts.warn) {
153
+ this.opts.warn(msg);
154
+ return;
155
+ }
156
+ if (process.env.AIDEN_VERBOSE !== '1')
157
+ return;
158
+ console.warn(`[auxiliary] ${msg}`);
126
159
  }
127
160
  async withTimeout(p, ms) {
128
161
  return new Promise((resolve, reject) => {
@@ -177,14 +177,19 @@ function createRealAgentRunner(opts) {
177
177
  let invocationError = null;
178
178
  try {
179
179
  result = await agent.runConversation(history, {
180
- // The agent honours its own abort signal via per-tool aborts;
181
- // tools that respect AbortSignal (shell_exec, fetch_*) will
182
- // bail when perTurnWatcher trips.
183
- //
184
- // Note: runConversation doesn't currently take an abort
185
- // signal in its options — the budget watcher is best-effort
186
- // observability via tally(). Future enhancement: thread the
187
- // signal into the loop body via options.
180
+ // The agent honours its own abort signal via per-tool aborts;
181
+ // tools that respect AbortSignal (shell_exec, fetch_*) will
182
+ // bail when perTurnWatcher trips.
183
+ //
184
+ // Note: runConversation doesn't currently take an abort
185
+ // signal in its options — the budget watcher is best-effort
186
+ // observability via tally(). Future enhancement: thread the
187
+ // signal into the loop body via options.
188
+ //
189
+ // v4.8.0 Phase 2.2 — uiOnly events on the daemon side are
190
+ // dropped here. Phase 2.4 will serialize them into the
191
+ // dispatcher's run_events stream.
192
+ onUiEvent: () => { },
188
193
  });
189
194
  // Stamp the actual token usage onto the watcher for the
190
195
  // post-turn snapshot below.
@@ -131,6 +131,42 @@ const EXECUTION_DISCIPLINE_PROSE = [
131
131
  'result. When the user requests an action, take it. When the user requests',
132
132
  'discussion, discuss.',
133
133
  ].join('\n');
134
+ /**
135
+ * v4.8.0 Phase 2.6 — UI events nudge. Without this, the model only
136
+ * emits ui_* tools when explicitly told to (e.g. "call ui_task_update
137
+ * with ..."). With it, events fire during normal multi-step work —
138
+ * research, file creation, test runs, command execution. Always-on:
139
+ * every model that sees the ui_* tools benefits.
140
+ */
141
+ const UI_EVENTS_GUIDANCE = [
142
+ '## UI events',
143
+ '',
144
+ 'When doing multi-step work, emit structured progress signals INSTEAD OF',
145
+ 'writing them as text. The user sees these as inline rows separate from',
146
+ 'your prose reply.',
147
+ '',
148
+ 'WRONG (do NOT do this):',
149
+ ' "✓ Done — found 3 results"',
150
+ ' "⟳ Searching the web..."',
151
+ ' "Created hello.py"',
152
+ '',
153
+ 'RIGHT:',
154
+ ' ui_task_update {task_id, label, status: "running"}',
155
+ ' ui_task_done {task_id, status: "success", summary}',
156
+ ' ui_artifact_created {path, kind: "file", preview}',
157
+ '',
158
+ 'When to fire each:',
159
+ '- ui_task_update + ui_task_done for any multi-step task (pair them by task_id)',
160
+ '- ui_command_result after shell_exec when the output is interesting',
161
+ '- ui_test_result after running tests',
162
+ '- ui_toast for transient notices (e.g. "switched to dark mode")',
163
+ '- ui_artifact_created when you create or modify a file/skill',
164
+ '- ui_approval_request fires automatically for risky tools — NEVER emit it manually',
165
+ '',
166
+ 'Markdown text in your reply is for explanation, not status. Status goes',
167
+ 'through events. Skip events entirely on single-shot queries that aren\'t',
168
+ 'multi-step work.',
169
+ ].join('\n');
134
170
  /**
135
171
  * Llama-3.3-specific tool-call format guard. Adapter-side recovery picks
136
172
  * up failures, but we'd rather avoid the 400 round-trip.
@@ -358,6 +394,15 @@ class PromptBuilder {
358
394
  optional: true,
359
395
  });
360
396
  }
397
+ // ── 6.6. UI events nudge (v4.8.0 Phase 2.6) ───────────────────────
398
+ // Unconditional like execution discipline — every model that sees
399
+ // the ui_* tools benefits. Teaches structured-event emission for
400
+ // multi-step work instead of relying on text status formatting.
401
+ slots.push({
402
+ name: 'uiEvents',
403
+ content: UI_EVENTS_GUIDANCE,
404
+ optional: true,
405
+ });
361
406
  // ── 7. Iteration budget ───────────────────────────────────────────
362
407
  if (opts.initialBudget) {
363
408
  const { used, max } = opts.initialBudget;
@@ -199,6 +199,7 @@ function buildChildAgent(deps, input) {
199
199
  resolveVerifiedFlag: deps.resolveVerifiedFlag,
200
200
  resolveToolset: deps.resolveToolset,
201
201
  resolveMutates: deps.resolveMutates,
202
+ resolveUiOnly: deps.resolveUiOnly,
202
203
  honestyEnforcement: childHonestyEnforcement,
203
204
  onToolCall,
204
205
  // iterationBudgetInjection inherits the default (true) — child
@@ -190,7 +190,13 @@ async function spawnSubAgent(spec, deps, ctx) {
190
190
  let tokensIn = 0;
191
191
  let tokensOut = 0;
192
192
  try {
193
- const result = await agentBundle.agent.runConversation(agentBundle.history, { signal: childCtrl.signal });
193
+ const result = await agentBundle.agent.runConversation(agentBundle.history, {
194
+ signal: childCtrl.signal,
195
+ // v4.8.0 Phase 2.2 — uiOnly events from a subagent are
196
+ // dropped. Subagents have no chat surface; the parent
197
+ // assembles their summary. Stub stays a no-op forever.
198
+ onUiEvent: () => { },
199
+ });
194
200
  apiCalls = result.turnCount; // one provider call per turn
195
201
  tokensIn = result.totalUsage.inputTokens;
196
202
  tokensOut = result.totalUsage.outputTokens;
@@ -90,29 +90,29 @@ function renderBanner(opts) {
90
90
  out.push('');
91
91
  return out.join('\n') + '\n';
92
92
  }
93
- // Wide layout: framed panel with ASCII art inside, taglines below.
93
+ // v4.8.0 Slice 10b — wide layout flows the AIDEN art without the
94
+ // heavy `╔══╗` frame (legacy chrome). The art carries its own
95
+ // visual weight as the boot-card identity anchor; framing it
96
+ // inside a closed box collides with the asymmetric orange-bar
97
+ // language used by every other v4.8.0 surface.
98
+ //
99
+ // v4.8.0 Slice 10c — emit raw 24-bit truecolor for the AIDEN art
100
+ // instead of routing through `c.primary` (which depth-detects via
101
+ // theme.ts and degrades to 256-color or 16-color when COLORTERM is
102
+ // unset — common on Windows ConPTY). Result on those terminals was
103
+ // a washed-out / grey AIDEN that didn't match the boot card's
104
+ // skinEngine-painted brand orange. Forcing truecolor here brings
105
+ // disclaimer + setupWizard banner in line with the boot card.
106
+ const ORANGE_ON = '\x1b[38;2;255;107;53m';
107
+ const COLOR_OFF = '\x1b[39m';
94
108
  const inner = w - 2;
95
109
  const artPad = Math.max(0, Math.floor((inner - ART_WIDTH) / 2));
96
- const framed = opts.framed !== false;
97
- const horiz = '═'.repeat(inner);
98
- const top = framed ? theme_1.c.rule(`╔${horiz}╗`) : '';
99
- const bottom = framed ? theme_1.c.rule(`╚${horiz}╝`) : '';
100
- const blank = framed
101
- ? `${theme_1.c.rule('║')}${' '.repeat(inner)}${theme_1.c.rule('║')}`
102
- : ' '.repeat(w);
103
110
  const lines = [];
104
111
  lines.push('');
105
- if (framed)
106
- lines.push(top);
107
- lines.push(blank);
108
112
  for (const row of AIDEN_ART) {
109
113
  const padded = rpad(' '.repeat(artPad) + row, inner);
110
- const coloured = theme_1.c.primary(padded);
111
- lines.push(framed ? `${theme_1.c.rule('║')}${coloured}${theme_1.c.rule('║')}` : ` ${coloured}`);
114
+ lines.push(` ${ORANGE_ON}${padded}${COLOR_OFF}`);
112
115
  }
113
- lines.push(blank);
114
- if (framed)
115
- lines.push(bottom);
116
116
  lines.push('');
117
117
  lines.push(' ' + (0, theme_1.dim)(theme_1.c.muted(versionLine)));
118
118
  lines.push('');
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // AUTO-GENERATED by scripts/inject-version.js — do not edit by hand
5
- exports.VERSION = '4.7.0';
5
+ exports.VERSION = '4.8.0';
@@ -237,6 +237,20 @@ class ApprovalEngine {
237
237
  this.callbacks.onDecision?.(req, 'deny');
238
238
  return false;
239
239
  }
240
+ // v4.8.0 Phase 2.5 — emit a structured ui_approval_request event
241
+ // BEFORE the y/n prompt fires. Additive: the display layer paints
242
+ // the gutter-integrated row, then the existing promptUser flow
243
+ // runs unchanged. Moat-tier (safe/caution/dangerous) maps to the
244
+ // ui schema's 4-tier scale; 'critical' is reserved for future
245
+ // wiring and unreachable from this path.
246
+ const uiTier = req.riskTier === 'safe' ? 'low' :
247
+ req.riskTier === 'dangerous' ? 'high' : 'medium';
248
+ const argsPreview = JSON.stringify(req.args).slice(0, 80);
249
+ this.callbacks.onUiEvent?.('ui_approval_request', {
250
+ prompt: `${req.toolName} ${argsPreview}`,
251
+ risk_tier: uiTier,
252
+ reason: req.reason,
253
+ });
240
254
  const decision = await this.callbacks.promptUser(req);
241
255
  this.callbacks.onDecision?.(req, decision);
242
256
  if (decision === 'deny')
@@ -246,6 +246,60 @@ function registerWriteTools(registry) {
246
246
  function registerAllTools(registry) {
247
247
  registerReadOnlyTools(registry);
248
248
  registerWriteTools(registry);
249
+ // v4.8.0 Phase 2.2 — register the 7 semantic ui_* event tools.
250
+ // All uiOnly: true → the dispatch loop in core/v4/aidenAgent.ts
251
+ // bypasses execute and fires onUiEvent on the caller. execute()
252
+ // throws as a safety guard: if the uiOnly branch ever misfires
253
+ // and an executor is reached, that's a wiring bug, not a render.
254
+ // Renderer is a no-op stub in this phase; Phase 2.3 lands chrome.
255
+ const ui = (name, description, properties, required) => ({
256
+ schema: { name, description, inputSchema: { type: 'object', properties, required } },
257
+ execute: async () => { throw new Error(`${name} is uiOnly — dispatch branch should bypass execute`); },
258
+ category: 'read', mutates: false, uiOnly: true,
259
+ });
260
+ const str = { type: 'string' };
261
+ const num = { type: 'number' };
262
+ registry.register(ui('ui_task_update', 'Signal current task state for the live task panel. Append-only stream.', { task_id: str, label: { type: 'string', description: '≤80 chars' },
263
+ status: { type: 'string', enum: ['running', 'blocked', 'paused'] },
264
+ kind: { type: 'string', enum: ['task', 'subagent'] }, depth: num, parent_id: str }, ['task_id', 'label', 'status']));
265
+ registry.register(ui('ui_task_done', 'Signal a task is complete. Pairs with a prior ui_task_update.', { task_id: str, status: { type: 'string', enum: ['success', 'failure', 'blocked'] },
266
+ summary: { type: 'string', description: 'Optional, ≤120 chars' } }, ['task_id', 'status']));
267
+ registry.register(ui('ui_command_result', 'Surface shell output as a formatted block.', { command: str, stdout: str, stderr: str, exit_code: num }, ['command']));
268
+ registry.register(ui('ui_test_result', 'Pass/fail count after a test run.', { framework: { type: 'string', description: 'e.g. "vitest", "pytest"' },
269
+ passed: num, failed: num, skipped: num, duration_ms: num }, ['framework', 'passed', 'failed']));
270
+ registry.register(ui('ui_approval_request', 'Structured approval prompt before a privileged action.', { prompt: { type: 'string', description: '≤160 chars' },
271
+ risk_tier: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
272
+ reason: { type: 'string', description: 'Optional, ≤200 chars' } }, ['prompt', 'risk_tier']));
273
+ registry.register(ui('ui_toast', 'Transient notice to surface without interrupting flow.', { message: { type: 'string', description: '≤120 chars' },
274
+ kind: { type: 'string', enum: ['info', 'success', 'warning', 'error'] } }, ['message', 'kind']));
275
+ registry.register(ui('ui_artifact_created', 'Surface a file or skill created/modified this turn.', { path: str, kind: { type: 'string', enum: ['file', 'skill', 'directory'] },
276
+ preview: { type: 'string', description: 'Optional, ≤200 chars' } }, ['path', 'kind']));
277
+ // v4.8.0 Phase 2.1 — env-gated uiOnly smoke stub. Never registers
278
+ // in production. Set AIDEN_TEST_UI_STUB=1 to enable for the
279
+ // dispatch-branch smoke harness. The execute() throws on purpose:
280
+ // if the uiOnly branch is wired correctly the model can never
281
+ // reach the executor.
282
+ if (process.env.AIDEN_TEST_UI_STUB === '1') {
283
+ registry.register({
284
+ schema: {
285
+ name: '_test_ui_stub',
286
+ description: 'Test-only uiOnly stub for v4.8.0 Phase 2.1 smoke. Set AIDEN_TEST_UI_STUB=1 to enable.',
287
+ inputSchema: {
288
+ type: 'object',
289
+ properties: {
290
+ message: { type: 'string', description: 'Arbitrary message to echo via onUiEvent' },
291
+ },
292
+ required: ['message'],
293
+ },
294
+ },
295
+ execute: async () => {
296
+ throw new Error('_test_ui_stub should never execute — uiOnly branch should bypass it');
297
+ },
298
+ category: 'read',
299
+ mutates: false,
300
+ uiOnly: true,
301
+ });
302
+ }
249
303
  }
250
304
  var subagentFanout_2 = require("./subagent/subagentFanout");
251
305
  Object.defineProperty(exports, "makeSubagentFanoutTool", { enumerable: true, get: function () { return subagentFanout_2.makeSubagentFanoutTool; } });
@@ -289,6 +289,19 @@ function makeSpawnSubAgentTool(factory) {
289
289
  // ── 3. Resolve optional parent run / session identifiers ─────────────
290
290
  const parentRunId = factory.resolveParentRunId?.();
291
291
  const parentSessionId = factory.resolveParentSessionId?.();
292
+ // v4.8.0 Phase 2.5 — emit ui_task_update for the subagent start.
293
+ // Stable task_id correlates with the matching ui_task_done emit
294
+ // after the spawnSubAgent call returns. depth:1 hardcoded today
295
+ // — childBuilder caps recursion at 1 (see SUBAGENT_BLOCKED_TOOL_NAMES
296
+ // 'spawn_sub_agent'). TODO: thread real depth when nested spawns ship.
297
+ const subTaskId = `subagent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
298
+ factory.onUiEvent?.('ui_task_update', {
299
+ task_id: subTaskId,
300
+ label: goalPreview,
301
+ status: 'running',
302
+ kind: 'subagent',
303
+ depth: 1,
304
+ });
292
305
  // ── 4. Invoke the primitive. NEVER throws — always envelope. ─────────
293
306
  const result = await (0, spawnSubAgent_1.spawnSubAgent)(spec, {
294
307
  // ChildBuilderDeps fields:
@@ -310,6 +323,16 @@ function makeSpawnSubAgentTool(factory) {
310
323
  parentRunId,
311
324
  parentSessionId,
312
325
  });
326
+ // v4.8.0 Phase 2.5 — emit ui_task_done with the same subTaskId
327
+ // so the display layer can finalize the in-flight row.
328
+ const doneStatus = result.ok ? 'success' :
329
+ result.status === 'interrupted' ? 'blocked' :
330
+ result.status === 'timeout' ? 'blocked' : 'failure';
331
+ factory.onUiEvent?.('ui_task_done', {
332
+ task_id: subTaskId,
333
+ status: doneStatus,
334
+ summary: `${result.metrics.apiCalls} calls · ${result.exitReason}`,
335
+ });
313
336
  // Completion log — pairs with "spawn_sub_agent invoked" so a
314
337
  // grep on parentSessionId surfaces invoke → complete in order.
315
338
  logger.info('spawn_sub_agent completed', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },