@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.
- package/CHANGELOG.md +1924 -1786
- package/README.md +2730 -2562
- package/dist/agent/controller.d.ts +49 -3
- package/dist/agent/controller.js +78 -3
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +49 -14
- package/dist/cli.js +214 -2
- package/dist/commands/branch-command.d.ts +61 -0
- package/dist/commands/branch-command.js +117 -0
- package/dist/commands/builtins.js +62 -40
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +48 -1
- package/dist/commands/session-command.d.ts +54 -0
- package/dist/commands/session-command.js +109 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/model-profiles.d.ts +8 -0
- package/dist/config/model-profiles.js +10 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +155 -0
- package/dist/proc/terminal-plan.js +360 -0
- package/dist/session/branch-invocation.d.ts +83 -0
- package/dist/session/branch-invocation.js +109 -0
- package/dist/session/branch-launch.d.ts +70 -0
- package/dist/session/branch-launch.js +248 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/session/cli-commands.d.ts +2 -0
- package/dist/session/cli-commands.js +36 -5
- package/dist/session/identity.d.ts +72 -0
- package/dist/session/identity.js +148 -0
- package/dist/session/limits.d.ts +66 -0
- package/dist/session/limits.js +66 -0
- package/dist/session/persist.d.ts +30 -5
- package/dist/session/startup-session.d.ts +107 -0
- package/dist/session/startup-session.js +315 -0
- package/dist/session/store.d.ts +60 -1
- package/dist/session/store.js +95 -6
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +154 -28
- package/dist/ui/Header.js +8 -16
- package/dist/ui/StatusBar.d.ts +8 -0
- package/dist/ui/StatusBar.js +12 -3
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/layout/header-hints.d.ts +68 -0
- package/dist/ui/layout/header-hints.js +138 -0
- package/dist/ui/layout/session-chip.d.ts +52 -0
- package/dist/ui/layout/session-chip.js +64 -0
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +8 -1
- package/dist/ui/overlays/HelpOverlay.js +17 -5
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -68
- package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
- package/dist/ui/overlays/SettingsScreen.js +34 -2
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +9 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- 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
|
|
552
|
-
* exception). `/todo` refuses mid-run before it gets here;
|
|
553
|
-
* on purpose
|
|
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;
|
package/dist/agent/controller.js
CHANGED
|
@@ -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
|
|
1449
|
-
* exception). `/todo` refuses mid-run before it gets here;
|
|
1450
|
-
* on purpose
|
|
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;
|