@aragon-agent/cli 0.6.24 → 0.6.26

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 (77) hide show
  1. package/CHANGELOG.md +1924 -1786
  2. package/README.md +2730 -2562
  3. package/dist/agent/controller.d.ts +49 -3
  4. package/dist/agent/controller.js +78 -3
  5. package/dist/agent/conversation-reset.d.ts +91 -0
  6. package/dist/agent/conversation-reset.js +66 -0
  7. package/dist/agent/model-probe-limits.d.ts +39 -0
  8. package/dist/agent/model-probe-limits.js +25 -0
  9. package/dist/agent/model-probe.d.ts +104 -0
  10. package/dist/agent/model-probe.js +391 -0
  11. package/dist/agent/reducer.d.ts +22 -0
  12. package/dist/agent/reducer.js +49 -14
  13. package/dist/cli.js +214 -2
  14. package/dist/commands/branch-command.d.ts +61 -0
  15. package/dist/commands/branch-command.js +117 -0
  16. package/dist/commands/builtins.js +62 -40
  17. package/dist/commands/clear-command.d.ts +27 -0
  18. package/dist/commands/clear-command.js +107 -0
  19. package/dist/commands/registry.d.ts +48 -1
  20. package/dist/commands/session-command.d.ts +54 -0
  21. package/dist/commands/session-command.js +109 -0
  22. package/dist/config/load.d.ts +15 -0
  23. package/dist/config/model-profiles.d.ts +8 -0
  24. package/dist/config/model-profiles.js +10 -0
  25. package/dist/exec/index.js +10 -1
  26. package/dist/logging/logger.d.ts +1 -1
  27. package/dist/proc/terminal-plan.d.ts +155 -0
  28. package/dist/proc/terminal-plan.js +360 -0
  29. package/dist/session/branch-invocation.d.ts +83 -0
  30. package/dist/session/branch-invocation.js +109 -0
  31. package/dist/session/branch-launch.d.ts +70 -0
  32. package/dist/session/branch-launch.js +248 -0
  33. package/dist/session/checkpoint.d.ts +257 -0
  34. package/dist/session/checkpoint.js +512 -0
  35. package/dist/session/clear-archive.d.ts +51 -0
  36. package/dist/session/clear-archive.js +160 -0
  37. package/dist/session/cli-commands.d.ts +2 -0
  38. package/dist/session/cli-commands.js +36 -5
  39. package/dist/session/identity.d.ts +72 -0
  40. package/dist/session/identity.js +148 -0
  41. package/dist/session/limits.d.ts +66 -0
  42. package/dist/session/limits.js +66 -0
  43. package/dist/session/persist.d.ts +30 -5
  44. package/dist/session/startup-session.d.ts +107 -0
  45. package/dist/session/startup-session.js +315 -0
  46. package/dist/session/store.d.ts +60 -1
  47. package/dist/session/store.js +95 -6
  48. package/dist/todo/types.d.ts +5 -3
  49. package/dist/ui/App.d.ts +54 -2
  50. package/dist/ui/App.js +154 -28
  51. package/dist/ui/Header.js +8 -16
  52. package/dist/ui/StatusBar.d.ts +8 -0
  53. package/dist/ui/StatusBar.js +12 -3
  54. package/dist/ui/entries/CompactionCard.d.ts +5 -4
  55. package/dist/ui/entries/CompactionCard.js +5 -4
  56. package/dist/ui/layout/header-hints.d.ts +68 -0
  57. package/dist/ui/layout/header-hints.js +138 -0
  58. package/dist/ui/layout/session-chip.d.ts +52 -0
  59. package/dist/ui/layout/session-chip.js +64 -0
  60. package/dist/ui/layout/status-layout.d.ts +10 -0
  61. package/dist/ui/layout/status-layout.js +8 -1
  62. package/dist/ui/overlays/HelpOverlay.js +17 -5
  63. package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
  64. package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
  65. package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
  66. package/dist/ui/overlays/ModelProfileEditor.js +117 -68
  67. package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
  68. package/dist/ui/overlays/SettingsScreen.js +34 -2
  69. package/dist/ui/overlays/model-probe-view.d.ts +128 -0
  70. package/dist/ui/overlays/model-probe-view.js +225 -0
  71. package/dist/ui/overlays/model-profile-state.d.ts +9 -1
  72. package/dist/ui/overlays/model-profile-state.js +10 -0
  73. package/dist/ui/overlays/use-model-probe.d.ts +30 -0
  74. package/dist/ui/overlays/use-model-probe.js +109 -0
  75. package/dist/ui/session-bridge.d.ts +26 -0
  76. package/dist/ui/session-bridge.js +32 -0
  77. package/package.json +2 -2
@@ -31,6 +31,7 @@ import type { CompactionEventListener, CompactionSnapshot, ContextUsageSnapshot
31
31
  import type { TodoEventListener, TodoSnapshot } from '../todo/types.js';
32
32
  import { type HumanInputGate } from '../tools/human-input.js';
33
33
  import { SkillService, type ApprovalGate } from '../skills/service.js';
34
+ import { type ConversationResetOptions, type ConversationResetResult } from './conversation-reset.js';
34
35
  import type { AgentMode } from './agent-mode.js';
35
36
  import type { NoticeLevel } from './reducer.js';
36
37
  /** Approval gate used whenever there is provably no human to ask (headless). */
@@ -548,9 +549,12 @@ export declare class AgentController {
548
549
  /** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
549
550
  restoreTodos(items: unknown): void;
550
551
  /**
551
- * `/todo clear` and `/clear`. A user override of a projection (I-2's standing
552
- * exception). `/todo` refuses mid-run before it gets here; `/clear` does not,
553
- * on purpose — refusing there would leave it unable to clear the transcript.
552
+ * `/todo clear` and `/clear screen`. A user override of a projection (I-2's
553
+ * standing exception). `/todo` refuses mid-run before it gets here;
554
+ * `/clear screen` does not, on purpose - refusing there would leave a user
555
+ * unable to clear the transcript mid-run. `/clear` itself is the conversation
556
+ * boundary: it reaches the store through `clearMessages()` with reason
557
+ * 'reset', after it has interrupted the run.
554
558
  */
555
559
  clearTodos(): void;
556
560
  getRetryConfig(): RetryConfig;
@@ -626,7 +630,49 @@ export declare class AgentController {
626
630
  * `subscribeTeam` / `subscribeTodos` / `subscribeFast` already set.
627
631
  */
628
632
  subscribeToolOutput(listener: ToolOutputListener): () => void;
633
+ /**
634
+ * Clear the conversation history: THE NEW-CONVERSATION BOUNDARY.
635
+ *
636
+ * Six things, and every one of them is session-scoped: cancel the idle
637
+ * compaction, drop the `/resume` identity, clear Core's history, bump
638
+ * `startupSequence`, deep-invalidate compaction and the meter, and clear the
639
+ * skill activation frames. `/resume` has the twin of this in `replaceMessages`,
640
+ * and `/clear` is this one's second caller - see
641
+ * `docs/plans/clear-command-context-reset/spec.md`.
642
+ */
629
643
  clearMessages(): void;
644
+ /**
645
+ * `/clear` (and therefore `/reset`) - the conversation reset, in the one order
646
+ * that is safe.
647
+ *
648
+ * A THIN ADAPTER AND NOTHING ELSE (D-7). The order - interrupt, bounded wait,
649
+ * archive, clear queues, clear history - lives in `agent/conversation-reset.ts`
650
+ * so it can be proven against a six-line fake instead of against a fully
651
+ * constructed controller. Everything below is a wiring decision, and two of
652
+ * them are load-bearing:
653
+ *
654
+ * `interruptRun` IS `forceStop()`, NOT `abort()` (P0-2 / C-1). `abort()` bumps
655
+ * `startupSequence` only, so the events the engine keeps emitting while it
656
+ * unwinds would be adopted by `App` as ordinary events of the new run - the
657
+ * first screen of a fresh conversation would show the previous task's text and
658
+ * tool cards, and the status bar would bounce back out of `idle`. `forceStop()`
659
+ * also increments `runGen`, which is the fence `App` compares every event
660
+ * against, and hard-kills a foreground `bash` that ignores the abort signal -
661
+ * without which the bounded wait below could only ever time out.
662
+ *
663
+ * The session is archived BEFORE the clear and never after (D-9 step 4),
664
+ * because after it there is nothing left to save.
665
+ */
666
+ resetConversation(opts?: ConversationResetOptions): Promise<ConversationResetResult>;
667
+ /**
668
+ * The four parts of the archive input this controller can answer itself.
669
+ *
670
+ * `entries` is NOT one of them (P0-1). The screen transcript lives in `App`'s
671
+ * `ViewState`, and teaching the controller about the view layer to obtain it
672
+ * would invert the dependency this class exists to keep. The command layer
673
+ * passes a thunk instead - see `CommandContext.transcriptEntries`.
674
+ */
675
+ private describeClearedConversation;
630
676
  /** Read-only view of the live system prompt (regression tests, /debug). */
631
677
  getSystemPrompt(): string;
632
678
  replaceMessages(messages: Message[], identity?: CompactionIdentity): void;
@@ -38,6 +38,8 @@ import { createNodeSkillHost } from '../skills/node-host.js';
38
38
  import { SkillService } from '../skills/service.js';
39
39
  import { createSkillTools } from '../skills/tools.js';
40
40
  import { buildSystemPrompt } from './system-prompt.js';
41
+ import { resetConversationWith, } from './conversation-reset.js';
42
+ import { archiveClearedConversation, } from '../session/clear-archive.js';
41
43
  /**
42
44
  * Extra budget, on top of `PROC_LIMITS.killGraceMs`, that `prompt()` allows a
43
45
  * force-stopped engine to unwind in before it starts a new run anyway (I-8).
@@ -1445,9 +1447,12 @@ export class AgentController {
1445
1447
  this.todos?.restore(items);
1446
1448
  }
1447
1449
  /**
1448
- * `/todo clear` and `/clear`. A user override of a projection (I-2's standing
1449
- * exception). `/todo` refuses mid-run before it gets here; `/clear` does not,
1450
- * on purpose — refusing there would leave it unable to clear the transcript.
1450
+ * `/todo clear` and `/clear screen`. A user override of a projection (I-2's
1451
+ * standing exception). `/todo` refuses mid-run before it gets here;
1452
+ * `/clear screen` does not, on purpose - refusing there would leave a user
1453
+ * unable to clear the transcript mid-run. `/clear` itself is the conversation
1454
+ * boundary: it reaches the store through `clearMessages()` with reason
1455
+ * 'reset', after it has interrupted the run.
1451
1456
  */
1452
1457
  clearTodos() {
1453
1458
  this.todos?.clear('user');
@@ -1697,6 +1702,16 @@ export class AgentController {
1697
1702
  this.outputListeners.delete(listener);
1698
1703
  };
1699
1704
  }
1705
+ /**
1706
+ * Clear the conversation history: THE NEW-CONVERSATION BOUNDARY.
1707
+ *
1708
+ * Six things, and every one of them is session-scoped: cancel the idle
1709
+ * compaction, drop the `/resume` identity, clear Core's history, bump
1710
+ * `startupSequence`, deep-invalidate compaction and the meter, and clear the
1711
+ * skill activation frames. `/resume` has the twin of this in `replaceMessages`,
1712
+ * and `/clear` is this one's second caller - see
1713
+ * `docs/plans/clear-command-context-reset/spec.md`.
1714
+ */
1700
1715
  clearMessages() {
1701
1716
  this.idleCompaction?.abort();
1702
1717
  this.restoredCompactionIdentity = undefined;
@@ -1723,6 +1738,66 @@ export class AgentController {
1723
1738
  // overriding a projection of a belief the model still holds.
1724
1739
  this.todos?.clear('reset');
1725
1740
  }
1741
+ /**
1742
+ * `/clear` (and therefore `/reset`) - the conversation reset, in the one order
1743
+ * that is safe.
1744
+ *
1745
+ * A THIN ADAPTER AND NOTHING ELSE (D-7). The order - interrupt, bounded wait,
1746
+ * archive, clear queues, clear history - lives in `agent/conversation-reset.ts`
1747
+ * so it can be proven against a six-line fake instead of against a fully
1748
+ * constructed controller. Everything below is a wiring decision, and two of
1749
+ * them are load-bearing:
1750
+ *
1751
+ * `interruptRun` IS `forceStop()`, NOT `abort()` (P0-2 / C-1). `abort()` bumps
1752
+ * `startupSequence` only, so the events the engine keeps emitting while it
1753
+ * unwinds would be adopted by `App` as ordinary events of the new run - the
1754
+ * first screen of a fresh conversation would show the previous task's text and
1755
+ * tool cards, and the status bar would bounce back out of `idle`. `forceStop()`
1756
+ * also increments `runGen`, which is the fence `App` compares every event
1757
+ * against, and hard-kills a foreground `bash` that ignores the abort signal -
1758
+ * without which the bounded wait below could only ever time out.
1759
+ *
1760
+ * The session is archived BEFORE the clear and never after (D-9 step 4),
1761
+ * because after it there is nothing left to save.
1762
+ */
1763
+ async resetConversation(opts = {}) {
1764
+ return resetConversationWith({
1765
+ isRunning: () => this.isRunning(),
1766
+ interruptRun: () => this.forceStop(),
1767
+ awaitEngineIdle: () => this.waitForEngineIdle().then(() => !this.isRunning()),
1768
+ archive: (readTranscript) => archiveClearedConversation({
1769
+ ...this.describeClearedConversation(),
1770
+ entries: readTranscript(),
1771
+ }),
1772
+ clearQueues: () => this.clearAllQueues(),
1773
+ clearHistory: () => {
1774
+ const cleared = this.agent.state.messages.length;
1775
+ this.clearMessages();
1776
+ return cleared;
1777
+ },
1778
+ }, opts);
1779
+ }
1780
+ /**
1781
+ * The four parts of the archive input this controller can answer itself.
1782
+ *
1783
+ * `entries` is NOT one of them (P0-1). The screen transcript lives in `App`'s
1784
+ * `ViewState`, and teaching the controller about the view layer to obtain it
1785
+ * would invert the dependency this class exists to keep. The command layer
1786
+ * passes a thunk instead - see `CommandContext.transcriptEntries`.
1787
+ */
1788
+ describeClearedConversation() {
1789
+ const snapshot = this.getSessionSnapshot();
1790
+ return {
1791
+ model: {
1792
+ providerId: this.config.provider,
1793
+ modelId: this.config.model,
1794
+ ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
1795
+ },
1796
+ messages: snapshot.messages,
1797
+ ...(snapshot.compactionIdentity ? { compactionIdentity: snapshot.compactionIdentity } : {}),
1798
+ todos: this.getTodoSnapshot()?.items ?? [],
1799
+ };
1800
+ }
1726
1801
  /** Read-only view of the live system prompt (regression tests, /debug). */
1727
1802
  getSystemPrompt() {
1728
1803
  return this.agent.state.systemPrompt;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The single authority on the ORDER of a conversation reset
3
+ * (clear-command-context-reset §3.4 / D-9).
4
+ *
5
+ * ASCII ONLY: `src/agent/**` is inside `glyphs.test.ts::inScope`, and this module
6
+ * contributes transcript text (the "could not stop" message) through the command
7
+ * layer.
8
+ *
9
+ * WHY A PORT RATHER THAN A METHOD ON `AgentController`: the order - interrupt,
10
+ * bounded wait, archive, clear queues, clear history - is the thing that has a
11
+ * correctness proof attached to it (a clear that lands before the engine unwinds
12
+ * pollutes the new conversation, or produces a `tool_use` with no `tool_result`).
13
+ * Written as a method, that order can only be tested by constructing a controller
14
+ * and everything it wires. Written against a port, the order itself is the thing
15
+ * under test, and every failure branch is reachable from a six-line fake.
16
+ *
17
+ * `interruptRun` IS NOT CALLED `abort`, ON PURPOSE (P0-2). It maps to
18
+ * `controller.forceStop()`, which does three things `abort()` does not: it
19
+ * hard-kills foreground `bash` children (the only thing that unblocks a tool that
20
+ * ignores the abort signal), and it bumps `runGen` - the generation fence `App`
21
+ * compares every engine event against. A method named `abort` that quietly does
22
+ * more than the existing `abort()` would be the exact kind of misleading name
23
+ * this feature exists to remove.
24
+ */
25
+ import type { Entry } from './reducer.js';
26
+ /** The minimal host surface a reset needs. Supplied by `AgentController` alone. */
27
+ export interface ConversationResetPort {
28
+ /** Whether a run is in flight right now. */
29
+ isRunning(): boolean;
30
+ /**
31
+ * Interrupt the current run: `controller.forceStop()` (P0-2).
32
+ *
33
+ * = `abort()` (stops team / fast / compaction) + `killForeground('force')`
34
+ * + `runGen += 1`. Background services are deliberately untouched.
35
+ */
36
+ interruptRun(): void;
37
+ /** Bounded wait for the engine to unwind; `true` means it settled. */
38
+ awaitEngineIdle(): Promise<boolean>;
39
+ /**
40
+ * Best-effort archive of the old conversation; an absolute path, or `null`.
41
+ *
42
+ * `readTranscript` is supplied by the caller because the screen transcript lives
43
+ * in `ViewState`, which the controller does not and must not know about (P0-1).
44
+ * Implementations never throw.
45
+ */
46
+ archive(readTranscript: () => Entry[]): string | null;
47
+ /** Drop Core's two queues plus the CLI's pending steering list. */
48
+ clearQueues(): void;
49
+ /** Clear the history (the six-part boundary) and return the message count. */
50
+ clearHistory(): number;
51
+ }
52
+ export interface ConversationResetOptions {
53
+ /** Interrupt a running engine (default `true`). `false` refuses instead. */
54
+ interrupt?: boolean;
55
+ /** Read the screen transcript for the archive. Absent = history-only archive. */
56
+ readTranscript?: () => Entry[];
57
+ }
58
+ export type ConversationResetResult = {
59
+ ok: true;
60
+ interrupted: boolean;
61
+ archivedPath: string | null;
62
+ messagesCleared: number;
63
+ } | {
64
+ ok: false;
65
+ reason: 'still-running';
66
+ message: string;
67
+ };
68
+ /**
69
+ * The one message a failed reset shows. It names the NEXT ACTION rather than the
70
+ * failure, because `forceStop()` already ran: what is left to try is the human's
71
+ * own hand.
72
+ */
73
+ export declare const CLEAR_STILL_RUNNING_MESSAGE = "Could not stop the current run. Press Esc twice to force-stop, then /clear.";
74
+ /**
75
+ * Reset the conversation, in the only order that is safe.
76
+ *
77
+ * THE ORDER IS THE DESIGN (D-9). Each step exists because of what happens if it
78
+ * moves:
79
+ * 1. interrupt - a clear that does not stop the loop first is overwritten by the
80
+ * loop's next `tool_result` append;
81
+ * 2. bounded wait - the interrupt is a request; `agent.abort()` resolves nothing
82
+ * by itself, so clearing now still races the unwind;
83
+ * 3. archive - BEFORE the clear, because after it there is nothing left to save;
84
+ * 4. clear queues - before the history, so a drain cannot re-feed the old queue;
85
+ * 5. clear history - the six-part boundary plus `startupSequence`.
86
+ *
87
+ * A FAILED WAIT CLEARS NOTHING (I-4). Half a reset - queues emptied, history
88
+ * still there - is worse than no reset at all, because the view and the engine
89
+ * then disagree with no visible cause.
90
+ */
91
+ export declare function resetConversationWith(port: ConversationResetPort, opts?: ConversationResetOptions): Promise<ConversationResetResult>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The single authority on the ORDER of a conversation reset
3
+ * (clear-command-context-reset §3.4 / D-9).
4
+ *
5
+ * ASCII ONLY: `src/agent/**` is inside `glyphs.test.ts::inScope`, and this module
6
+ * contributes transcript text (the "could not stop" message) through the command
7
+ * layer.
8
+ *
9
+ * WHY A PORT RATHER THAN A METHOD ON `AgentController`: the order - interrupt,
10
+ * bounded wait, archive, clear queues, clear history - is the thing that has a
11
+ * correctness proof attached to it (a clear that lands before the engine unwinds
12
+ * pollutes the new conversation, or produces a `tool_use` with no `tool_result`).
13
+ * Written as a method, that order can only be tested by constructing a controller
14
+ * and everything it wires. Written against a port, the order itself is the thing
15
+ * under test, and every failure branch is reachable from a six-line fake.
16
+ *
17
+ * `interruptRun` IS NOT CALLED `abort`, ON PURPOSE (P0-2). It maps to
18
+ * `controller.forceStop()`, which does three things `abort()` does not: it
19
+ * hard-kills foreground `bash` children (the only thing that unblocks a tool that
20
+ * ignores the abort signal), and it bumps `runGen` - the generation fence `App`
21
+ * compares every engine event against. A method named `abort` that quietly does
22
+ * more than the existing `abort()` would be the exact kind of misleading name
23
+ * this feature exists to remove.
24
+ */
25
+ /**
26
+ * The one message a failed reset shows. It names the NEXT ACTION rather than the
27
+ * failure, because `forceStop()` already ran: what is left to try is the human's
28
+ * own hand.
29
+ */
30
+ export const CLEAR_STILL_RUNNING_MESSAGE = 'Could not stop the current run. Press Esc twice to force-stop, then /clear.';
31
+ /**
32
+ * Reset the conversation, in the only order that is safe.
33
+ *
34
+ * THE ORDER IS THE DESIGN (D-9). Each step exists because of what happens if it
35
+ * moves:
36
+ * 1. interrupt - a clear that does not stop the loop first is overwritten by the
37
+ * loop's next `tool_result` append;
38
+ * 2. bounded wait - the interrupt is a request; `agent.abort()` resolves nothing
39
+ * by itself, so clearing now still races the unwind;
40
+ * 3. archive - BEFORE the clear, because after it there is nothing left to save;
41
+ * 4. clear queues - before the history, so a drain cannot re-feed the old queue;
42
+ * 5. clear history - the six-part boundary plus `startupSequence`.
43
+ *
44
+ * A FAILED WAIT CLEARS NOTHING (I-4). Half a reset - queues emptied, history
45
+ * still there - is worse than no reset at all, because the view and the engine
46
+ * then disagree with no visible cause.
47
+ */
48
+ export async function resetConversationWith(port, opts = {}) {
49
+ const interrupt = opts.interrupt ?? true;
50
+ const running = port.isRunning();
51
+ if (running && !interrupt) {
52
+ return { ok: false, reason: 'still-running', message: CLEAR_STILL_RUNNING_MESSAGE };
53
+ }
54
+ if (running) {
55
+ port.interruptRun();
56
+ const settled = await port.awaitEngineIdle();
57
+ if (!settled) {
58
+ return { ok: false, reason: 'still-running', message: CLEAR_STILL_RUNNING_MESSAGE };
59
+ }
60
+ }
61
+ const readTranscript = opts.readTranscript ?? (() => []);
62
+ const archivedPath = port.archive(readTranscript);
63
+ port.clearQueues();
64
+ const messagesCleared = port.clearHistory();
65
+ return { ok: true, interrupted: running, archivedPath, messagesCleared };
66
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Structural bounds for the profile connection probe (model-profile-connection-test §7).
3
+ *
4
+ * STRUCTURAL, NOT POLICY: nothing here is clamped, persisted or user-tunable. These
5
+ * are the physical limits of one probe - how long a single round trip may take, how
6
+ * many lines and characters a result may occupy, how many models a list page can
7
+ * carry before it must truncate. The user-tunable keys live in `config/schema.ts`.
8
+ *
9
+ * THE OUTPUT-TOKEN CAP IS DELIBERATELY ABSENT. It must come from core's
10
+ * `MIN_MAX_OUTPUT_TOKENS`, or this file becomes a second authority for "how many
11
+ * tokens may this model produce" - which is exactly what `output-limits.ts` exists
12
+ * to prevent.
13
+ *
14
+ * ASCII ONLY: this module lives in `agent/`, which `glyphs.test.ts::inScope` scans.
15
+ */
16
+ export interface ProbeLimits {
17
+ /** Ceiling on ONE real round trip (`complete`). */
18
+ testTimeoutMs: number;
19
+ /**
20
+ * Sub-budget for the best-effort model list that runs CONCURRENTLY with the
21
+ * completion. It never decides the verdict (§4.3 / RV-P1-1): a slow `/models`
22
+ * that ate the adapter's own 15 s discovery timeout would turn a healthy
23
+ * connection into a false `timeout`.
24
+ */
25
+ listModelsBudgetMs: number;
26
+ /** Ceiling on a discovery (`listModels`) issued from the list page. */
27
+ discoveryTimeoutMs: number;
28
+ /** Characters of the model's reply kept for the `Reply:` line. */
29
+ replyChars: number;
30
+ /** Characters of a locally composed failure sentence. */
31
+ detailChars: number;
32
+ /** Rows a list page may carry before `truncated` becomes true. */
33
+ maxListedModels: number;
34
+ /** Above this many rows the list page grows a search line (picker parity). */
35
+ searchThreshold: number;
36
+ /** Characters of a model's display name kept in one list row. */
37
+ maxNameChars: number;
38
+ }
39
+ export declare const PROBE_LIMITS: ProbeLimits;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Structural bounds for the profile connection probe (model-profile-connection-test §7).
3
+ *
4
+ * STRUCTURAL, NOT POLICY: nothing here is clamped, persisted or user-tunable. These
5
+ * are the physical limits of one probe - how long a single round trip may take, how
6
+ * many lines and characters a result may occupy, how many models a list page can
7
+ * carry before it must truncate. The user-tunable keys live in `config/schema.ts`.
8
+ *
9
+ * THE OUTPUT-TOKEN CAP IS DELIBERATELY ABSENT. It must come from core's
10
+ * `MIN_MAX_OUTPUT_TOKENS`, or this file becomes a second authority for "how many
11
+ * tokens may this model produce" - which is exactly what `output-limits.ts` exists
12
+ * to prevent.
13
+ *
14
+ * ASCII ONLY: this module lives in `agent/`, which `glyphs.test.ts::inScope` scans.
15
+ */
16
+ export const PROBE_LIMITS = {
17
+ testTimeoutMs: 20_000,
18
+ listModelsBudgetMs: 5_000,
19
+ discoveryTimeoutMs: 20_000,
20
+ replyChars: 60,
21
+ detailChars: 160,
22
+ maxListedModels: 400,
23
+ searchThreshold: 8,
24
+ maxNameChars: 48,
25
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The connection probe behind `Test connection` and `Discover models`
3
+ * (model-profile-connection-test §4.2-§4.8).
4
+ *
5
+ * WHY THIS IS NOT `ModelRegistry.discoverModels()`: that path folds every exception
6
+ * into `[]` and stores the result in a five-minute process-wide cache. Using it here
7
+ * would (a) collapse "wrong key", "wrong URL" and "this account owns no models" into
8
+ * one sentence, and (b) let a probe of an UNSAVED draft endpoint poison `/model`,
9
+ * `/context` and the main session's window resolution for five minutes. So the probe
10
+ * talks to the adapter directly and classifies by `LLMError.errorType`.
11
+ *
12
+ * WHY `complete()` AND NOT ONLY `listModels()`: a model can be listed and still be
13
+ * unable to generate (gateway aliases, permission layers). One real round trip is the
14
+ * only honest answer to "does this work right now", and it is also the only evidence
15
+ * that can produce `model_not_found`.
16
+ *
17
+ * ASCII ONLY: `agent/` is inside `glyphs.test.ts::inScope`.
18
+ */
19
+ import { type ProviderRegistry } from '@aragon-agent/core';
20
+ /** Every way a probe can fail, once and for all (§7). */
21
+ export type ProbeFailureKind = 'missing_key' | 'unsupported_provider' | 'invalid_base_url' | 'auth' | 'model_not_found' | 'invalid_request' | 'rate_limit' | 'provider_unavailable' | 'network' | 'timeout' | 'cancelled' | 'unknown';
22
+ export interface ProbeTarget {
23
+ provider: string;
24
+ /** `''` means "no model typed yet" - the probe then only lists models. */
25
+ model: string;
26
+ baseUrl: string | null;
27
+ /** The RESOLVED plaintext key from `resolveDraftKey()`, never `editor.apiKey`. */
28
+ apiKey: string | null;
29
+ }
30
+ export interface ProbeRequest extends ProbeTarget {
31
+ signal?: AbortSignal;
32
+ }
33
+ export interface ProbeOutcome {
34
+ ok: boolean;
35
+ /** Fingerprint of the target this outcome describes (§4.6). */
36
+ targetId: string;
37
+ latencyMs: number;
38
+ kind?: ProbeFailureKind;
39
+ /** Locally composed, already redacted sentence. Never provider response text. */
40
+ detail?: string;
41
+ reply?: string;
42
+ usage?: {
43
+ inputTokens: number;
44
+ outputTokens: number;
45
+ };
46
+ availableModels?: number;
47
+ modelListed?: boolean;
48
+ }
49
+ export interface ModelCandidate {
50
+ id: string;
51
+ name: string;
52
+ contextWindow?: number;
53
+ supportsTools: boolean;
54
+ }
55
+ export interface DiscoveryOutcome {
56
+ ok: boolean;
57
+ connectionId: string;
58
+ kind?: ProbeFailureKind;
59
+ detail?: string;
60
+ models: ModelCandidate[];
61
+ truncated: boolean;
62
+ /**
63
+ * How many models the endpoint returned BEFORE truncation. The truncation line
64
+ * needs the real number ("Showing first 400 of 960"), and re-deriving it from a
65
+ * clamped array is impossible.
66
+ */
67
+ total?: number;
68
+ }
69
+ export interface ModelProbe {
70
+ test(request: ProbeRequest): Promise<ProbeOutcome>;
71
+ discover(request: ProbeRequest): Promise<DiscoveryOutcome>;
72
+ targetFingerprint(target: ProbeTarget): string;
73
+ connectionFingerprint(target: ProbeTarget): string;
74
+ }
75
+ export interface ProbeDeps {
76
+ registry: Pick<ProviderRegistry, 'get'>;
77
+ /** Injected clock; tests pin `latencyMs` through it. */
78
+ now?: () => number;
79
+ }
80
+ /**
81
+ * Which key this draft would actually use (§4.2.1).
82
+ *
83
+ * The editor's `apiKey` field is ALWAYS the empty string: it is the replacement
84
+ * input, not the live credential. `finishProfileEditor` resolves `keep` to the
85
+ * saved entry key and `shared` to `null`, and the runtime then falls back to the
86
+ * shared per-provider key. Without this table the most common path - editing an
87
+ * existing profile, whose credential mode defaults to `keep` - would report
88
+ * `(no key)` for a profile that has one.
89
+ */
90
+ export declare function resolveDraftKey(input: {
91
+ credentialMode: 'keep' | 'replace' | 'shared';
92
+ draftKey: string;
93
+ existingKey: string | null;
94
+ sharedKey?: string;
95
+ }): string | null;
96
+ /**
97
+ * The host a failure sentence names - same four clauses as
98
+ * `profileEndpointSummary()` in `ui/overlays/ModelProfilePicker.tsx`, which this
99
+ * layer may not import (agent -> ui would invert the layering). `model-probe.test.ts`
100
+ * asserts the two agree, so the duplication cannot drift silently.
101
+ */
102
+ export declare function endpointHost(baseUrl: string | null): string;
103
+ /** Create the probe port. One instance per App; `SettingsScreen` only passes it down. */
104
+ export declare function createModelProbe(deps: ProbeDeps): ModelProbe;