@ocis/myagent-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +357 -0
  2. package/dist/agent/context.d.ts +33 -0
  3. package/dist/agent/context.js +169 -0
  4. package/dist/agent/modes.d.ts +21 -0
  5. package/dist/agent/modes.js +84 -0
  6. package/dist/agent/prompt-builder.d.ts +9 -0
  7. package/dist/agent/prompt-builder.js +31 -0
  8. package/dist/agent/sessions.d.ts +38 -0
  9. package/dist/agent/sessions.js +130 -0
  10. package/dist/agent/todo.d.ts +18 -0
  11. package/dist/agent/todo.js +61 -0
  12. package/dist/agent/turn.d.ts +309 -0
  13. package/dist/agent/turn.js +1253 -0
  14. package/dist/approval/policy.d.ts +81 -0
  15. package/dist/approval/policy.js +157 -0
  16. package/dist/config.d.ts +49 -0
  17. package/dist/config.js +156 -0
  18. package/dist/git/status.d.ts +89 -0
  19. package/dist/git/status.js +226 -0
  20. package/dist/headless.d.ts +72 -0
  21. package/dist/headless.js +330 -0
  22. package/dist/index.d.ts +60 -0
  23. package/dist/index.js +511 -0
  24. package/dist/protocol/client.d.ts +123 -0
  25. package/dist/protocol/client.js +250 -0
  26. package/dist/protocol/sse-frames.d.ts +6 -0
  27. package/dist/protocol/sse-frames.js +75 -0
  28. package/dist/protocol/types.d.ts +200 -0
  29. package/dist/protocol/types.js +8 -0
  30. package/dist/runtime.d.ts +38 -0
  31. package/dist/runtime.js +166 -0
  32. package/dist/sanitize.d.ts +1 -0
  33. package/dist/sanitize.js +21 -0
  34. package/dist/skills/discovery.d.ts +24 -0
  35. package/dist/skills/discovery.js +109 -0
  36. package/dist/tools/binary.d.ts +2 -0
  37. package/dist/tools/binary.js +22 -0
  38. package/dist/tools/diff.d.ts +1 -0
  39. package/dist/tools/diff.js +49 -0
  40. package/dist/tools/find.d.ts +2 -0
  41. package/dist/tools/find.js +61 -0
  42. package/dist/tools/fs.d.ts +2 -0
  43. package/dist/tools/fs.js +276 -0
  44. package/dist/tools/glob.d.ts +6 -0
  45. package/dist/tools/glob.js +131 -0
  46. package/dist/tools/grep.d.ts +3 -0
  47. package/dist/tools/grep.js +228 -0
  48. package/dist/tools/paths.d.ts +27 -0
  49. package/dist/tools/paths.js +124 -0
  50. package/dist/tools/registry.d.ts +13 -0
  51. package/dist/tools/registry.js +38 -0
  52. package/dist/tools/shell.d.ts +2 -0
  53. package/dist/tools/shell.js +136 -0
  54. package/dist/tools/skills.d.ts +2 -0
  55. package/dist/tools/skills.js +36 -0
  56. package/dist/tools/todo.d.ts +2 -0
  57. package/dist/tools/todo.js +43 -0
  58. package/dist/tools/transfer.d.ts +2 -0
  59. package/dist/tools/transfer.js +145 -0
  60. package/dist/tools/truncate.d.ts +12 -0
  61. package/dist/tools/truncate.js +46 -0
  62. package/dist/tools/types.d.ts +85 -0
  63. package/dist/tools/types.js +63 -0
  64. package/dist/ui/app.d.ts +39 -0
  65. package/dist/ui/app.js +1061 -0
  66. package/dist/ui/colors.d.ts +100 -0
  67. package/dist/ui/colors.js +169 -0
  68. package/dist/ui/components.d.ts +267 -0
  69. package/dist/ui/components.js +811 -0
  70. package/dist/ui/diff.d.ts +37 -0
  71. package/dist/ui/diff.js +143 -0
  72. package/dist/ui/format.d.ts +28 -0
  73. package/dist/ui/format.js +76 -0
  74. package/dist/ui/help.d.ts +6 -0
  75. package/dist/ui/help.js +45 -0
  76. package/dist/ui/highlight.d.ts +20 -0
  77. package/dist/ui/highlight.js +210 -0
  78. package/dist/ui/logo.d.ts +24 -0
  79. package/dist/ui/logo.js +106 -0
  80. package/dist/ui/model-list.d.ts +10 -0
  81. package/dist/ui/model-list.js +33 -0
  82. package/dist/ui/quit-confirm.d.ts +8 -0
  83. package/dist/ui/quit-confirm.js +40 -0
  84. package/dist/ui/select-popup.d.ts +30 -0
  85. package/dist/ui/select-popup.js +54 -0
  86. package/dist/ui/theme.d.ts +4 -0
  87. package/dist/ui/theme.js +41 -0
  88. package/dist/ui/tool-view.d.ts +20 -0
  89. package/dist/ui/tool-view.js +326 -0
  90. package/package.json +44 -0
  91. package/skills/git-commit/SKILL.md +27 -0
@@ -0,0 +1,9 @@
1
+ export interface PromptBuilderOptions {
2
+ workspace: string;
3
+ skillDirs: string[];
4
+ /** Skip AGENTS.md / CLAUDE.md discovery (--no-context-files). */
5
+ noContextFiles?: boolean;
6
+ extraInstructions?: string;
7
+ }
8
+ /** Returns a `buildSystemPrompt()` function for AgentRunnerOptions. */
9
+ export declare function createSystemPromptBuilder(opts: PromptBuilderOptions): () => Promise<string>;
@@ -0,0 +1,31 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared system-prompt builder for the interactive and headless runners.
3
+ //
4
+ // Both entry points ship the same block: context files discovered on the
5
+ // machine, environment facts (workspace + OS), the local skills catalog and
6
+ // the static mode protocol. The block is deliberately independent
7
+ // of the active mode and other per-turn state (todos, git branch, date) so it
8
+ // stays byte-identical across turns and the provider prompt cache holds.
9
+ // ---------------------------------------------------------------------------
10
+ import { composeSystemPrompt, findContextFiles, osDescription } from "./context.js";
11
+ import { buildSkillsCatalog, discoverSkills } from "../skills/discovery.js";
12
+ import { globalContextPath } from "../config.js";
13
+ /** Returns a `buildSystemPrompt()` function for AgentRunnerOptions. */
14
+ export function createSystemPromptBuilder(opts) {
15
+ return async () => {
16
+ const contextFiles = opts.noContextFiles
17
+ ? []
18
+ : await findContextFiles(opts.workspace, globalContextPath());
19
+ const skills = await discoverSkills(opts.skillDirs);
20
+ return composeSystemPrompt({
21
+ workspace: opts.workspace,
22
+ contextFiles,
23
+ environment: {
24
+ workspace: opts.workspace,
25
+ os: osDescription(),
26
+ },
27
+ skillsCatalog: buildSkillsCatalog(skills),
28
+ extraInstructions: opts.extraInstructions,
29
+ });
30
+ };
31
+ }
@@ -0,0 +1,38 @@
1
+ import type { TodoItem } from "./todo.js";
2
+ export interface LocalSessionEntry {
3
+ id: string;
4
+ title: string;
5
+ updatedAt: number;
6
+ todos?: TodoItem[];
7
+ }
8
+ export interface LocalSessionIndexData {
9
+ [cwd: string]: {
10
+ last?: string;
11
+ sessions: LocalSessionEntry[];
12
+ };
13
+ }
14
+ export declare class SessionIndex {
15
+ private readonly path;
16
+ private data;
17
+ constructor(path: string, data?: LocalSessionIndexData);
18
+ static load(path: string): Promise<SessionIndex>;
19
+ private bucket;
20
+ /** Record/refresh a thread for a working directory and mark it most recent. */
21
+ remember(cwd: string, threadId: string, title: string): void;
22
+ lastThread(cwd: string): string | undefined;
23
+ get(cwd: string, threadId: string): LocalSessionEntry | undefined;
24
+ list(cwd: string): LocalSessionEntry[];
25
+ /** Thread ids this machine has seen for a working directory. */
26
+ knownThreadIds(cwd: string): Set<string>;
27
+ /** Reverse lookup: which working directory recorded this thread. */
28
+ findCwd(threadId: string): string | undefined;
29
+ setTitle(cwd: string, threadId: string, title: string): void;
30
+ setTodos(cwd: string, threadId: string, todos: TodoItem[]): void;
31
+ forget(cwd: string, threadId: string): void;
32
+ /** Fire-and-forget persistence, serialized so concurrent updates can't interleave. */
33
+ save(): Promise<void>;
34
+ private saveChain;
35
+ private write;
36
+ }
37
+ /** Derive a session title from the first user message (locally, before PATCH). */
38
+ export declare function deriveTitle(message: string): string;
@@ -0,0 +1,130 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Local session index: cwd → known remote threads (+ per-thread TODO lists).
3
+ //
4
+ // The remote session is the source of truth for history; this file only
5
+ // remembers which thread belongs to which working directory so `-c`/`-r`
6
+ // resume the right conversation. Never contains credentials.
7
+ // ---------------------------------------------------------------------------
8
+ import { mkdir, rename } from "node:fs/promises";
9
+ import { dirname } from "node:path";
10
+ import { fileExists, readJsonFile, writeFile } from "../runtime.js";
11
+ export class SessionIndex {
12
+ path;
13
+ data;
14
+ constructor(path, data = {}) {
15
+ this.path = path;
16
+ this.data = data;
17
+ }
18
+ static async load(path) {
19
+ try {
20
+ if (!(await fileExists(path)))
21
+ return new SessionIndex(path);
22
+ const parsed = await readJsonFile(path);
23
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
24
+ return new SessionIndex(path);
25
+ // Normalize instead of trusting the persisted shape: a truncated write
26
+ // or a manual edit can leave a bucket without a valid `sessions` array,
27
+ // which would crash the first remember()/list() on that cwd.
28
+ const data = {};
29
+ for (const [cwd, bucket] of Object.entries(parsed)) {
30
+ if (!bucket || typeof bucket !== "object")
31
+ continue;
32
+ data[cwd] = {
33
+ last: typeof bucket.last === "string" ? bucket.last : undefined,
34
+ sessions: Array.isArray(bucket.sessions)
35
+ ? bucket.sessions.filter((s) => s && typeof s === "object" && typeof s.id === "string")
36
+ : [],
37
+ };
38
+ }
39
+ return new SessionIndex(path, data);
40
+ }
41
+ catch {
42
+ return new SessionIndex(path);
43
+ }
44
+ }
45
+ bucket(cwd) {
46
+ return (this.data[cwd] ??= { sessions: [] });
47
+ }
48
+ /** Record/refresh a thread for a working directory and mark it most recent. */
49
+ remember(cwd, threadId, title) {
50
+ const bucket = this.bucket(cwd);
51
+ const existing = bucket.sessions.find((s) => s.id === threadId);
52
+ const entry = existing
53
+ ? { ...existing, title: title || existing.title, updatedAt: Date.now() }
54
+ : { id: threadId, title, updatedAt: Date.now() };
55
+ // Move to the front (recency order) — no timestamp sorting, which would be
56
+ // non-deterministic for same-millisecond updates.
57
+ bucket.sessions = [entry, ...bucket.sessions.filter((s) => s.id !== threadId)].slice(0, 50);
58
+ bucket.last = threadId;
59
+ void this.save();
60
+ }
61
+ lastThread(cwd) {
62
+ return this.data[cwd]?.last;
63
+ }
64
+ get(cwd, threadId) {
65
+ return this.data[cwd]?.sessions.find((s) => s.id === threadId);
66
+ }
67
+ list(cwd) {
68
+ return [...(this.data[cwd]?.sessions ?? [])];
69
+ }
70
+ /** Thread ids this machine has seen for a working directory. */
71
+ knownThreadIds(cwd) {
72
+ return new Set((this.data[cwd]?.sessions ?? []).map((s) => s.id));
73
+ }
74
+ /** Reverse lookup: which working directory recorded this thread. */
75
+ findCwd(threadId) {
76
+ for (const [cwd, bucket] of Object.entries(this.data)) {
77
+ if (bucket.sessions.some((s) => s.id === threadId))
78
+ return cwd;
79
+ }
80
+ return undefined;
81
+ }
82
+ setTitle(cwd, threadId, title) {
83
+ const entry = this.get(cwd, threadId);
84
+ if (entry) {
85
+ entry.title = title;
86
+ void this.save();
87
+ }
88
+ }
89
+ setTodos(cwd, threadId, todos) {
90
+ const entry = this.get(cwd, threadId);
91
+ if (entry) {
92
+ entry.todos = todos;
93
+ void this.save();
94
+ }
95
+ }
96
+ forget(cwd, threadId) {
97
+ const bucket = this.data[cwd];
98
+ if (!bucket)
99
+ return;
100
+ bucket.sessions = bucket.sessions.filter((s) => s.id !== threadId);
101
+ if (bucket.last === threadId)
102
+ bucket.last = bucket.sessions[0]?.id;
103
+ void this.save();
104
+ }
105
+ /** Fire-and-forget persistence, serialized so concurrent updates can't interleave. */
106
+ save() {
107
+ this.saveChain = this.saveChain
108
+ .then(() => this.write())
109
+ .catch(() => { });
110
+ return this.saveChain;
111
+ }
112
+ saveChain = Promise.resolve();
113
+ async write() {
114
+ await mkdir(dirname(this.path), { recursive: true });
115
+ // Per-write unique temp name: two CLI processes sharing the index (two
116
+ // terminal windows) would otherwise interleave on one fixed `.tmp` path
117
+ // and could rename a half-written file over the index.
118
+ const tmp = `${this.path}.${process.pid}.${crypto.randomUUID()}.tmp`;
119
+ await writeFile(tmp, JSON.stringify(this.data, null, 2) + "\n");
120
+ await rename(tmp, this.path);
121
+ }
122
+ }
123
+ /** Derive a session title from the first user message (locally, before PATCH). */
124
+ export function deriveTitle(message) {
125
+ const firstLine = message.trim().split("\n")[0] ?? "";
126
+ const clean = firstLine.replace(/\s+/g, " ").trim();
127
+ if (!clean)
128
+ return "Untitled";
129
+ return clean.length > 60 ? `${clean.slice(0, 57)}…` : clean;
130
+ }
@@ -0,0 +1,18 @@
1
+ export type TodoStatus = "pending" | "in_progress" | "completed";
2
+ export interface TodoItem {
3
+ content: string;
4
+ status: TodoStatus;
5
+ }
6
+ /** Parse/validate the tool's `todos` argument. Throws with a model-readable message. */
7
+ export declare function parseTodoItems(value: unknown): TodoItem[];
8
+ export declare class TodoStore {
9
+ private items;
10
+ private listeners;
11
+ list(): readonly TodoItem[];
12
+ get doneCount(): number;
13
+ get current(): TodoItem | undefined;
14
+ replace(items: TodoItem[]): void;
15
+ onChanged(listener: () => void): () => void;
16
+ toJSON(): TodoItem[];
17
+ static fromJSON(value: unknown): TodoStore;
18
+ }
@@ -0,0 +1,61 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Per-session TODO list, driven by the local_update_todo client tool.
3
+ //
4
+ // In-memory with a change listener for the TUI panel; the session index
5
+ // persists it so resume restores the same list. The agent is instructed to
6
+ // send the ENTIRE list on every update (same contract as myagent-studio).
7
+ // ---------------------------------------------------------------------------
8
+ const VALID_STATUSES = ["pending", "in_progress", "completed"];
9
+ /** Parse/validate the tool's `todos` argument. Throws with a model-readable message. */
10
+ export function parseTodoItems(value) {
11
+ if (!Array.isArray(value))
12
+ throw new Error("`todos` must be an array of {content, status} items.");
13
+ return value.map((raw, index) => {
14
+ if (typeof raw !== "object" || raw === null)
15
+ throw new Error(`todos[${index}] must be an object.`);
16
+ const item = raw;
17
+ if (typeof item.content !== "string" || !item.content.trim()) {
18
+ throw new Error(`todos[${index}].content must be a non-empty string.`);
19
+ }
20
+ if (typeof item.status !== "string" || !VALID_STATUSES.includes(item.status)) {
21
+ throw new Error(`todos[${index}].status must be one of: ${VALID_STATUSES.join(", ")}.`);
22
+ }
23
+ return { content: item.content.trim(), status: item.status };
24
+ });
25
+ }
26
+ export class TodoStore {
27
+ items = [];
28
+ listeners = new Set();
29
+ list() {
30
+ return this.items;
31
+ }
32
+ get doneCount() {
33
+ return this.items.filter((t) => t.status === "completed").length;
34
+ }
35
+ get current() {
36
+ return this.items.find((t) => t.status === "in_progress")
37
+ ?? this.items.find((t) => t.status === "pending");
38
+ }
39
+ replace(items) {
40
+ this.items = items;
41
+ for (const listener of this.listeners)
42
+ listener();
43
+ }
44
+ onChanged(listener) {
45
+ this.listeners.add(listener);
46
+ return () => this.listeners.delete(listener);
47
+ }
48
+ toJSON() {
49
+ return this.items;
50
+ }
51
+ static fromJSON(value) {
52
+ const store = new TodoStore();
53
+ if (Array.isArray(value)) {
54
+ try {
55
+ store.items = parseTodoItems(value);
56
+ }
57
+ catch { /* ignore corrupt persisted list */ }
58
+ }
59
+ return store;
60
+ }
61
+ }
@@ -0,0 +1,309 @@
1
+ import type { IntegrationClient } from "../protocol/client.js";
2
+ import type { AgentUsage, ClientToolCall, ServerToolEvent, SessionDetail } from "../protocol/types.js";
3
+ import type { ToolRegistry } from "../tools/registry.js";
4
+ import type { AgentMode } from "./modes.js";
5
+ import type { TodoStore } from "./todo.js";
6
+ import type { ApprovalPolicy } from "../approval/policy.js";
7
+ export type RunState = "idle" | "running" | "stopping";
8
+ /** True for event-channel failures retrying cannot fix (bad key, deleted session). */
9
+ export declare function isPermanentEventChannelError(err: unknown): boolean;
10
+ export interface TurnCallbacks {
11
+ onUserMessage: (text: string, queued: boolean) => void;
12
+ /**
13
+ * A message submitted while a run is active. It is NOT part of the
14
+ * conversation yet — the UI floats it above the prompt until the agent
15
+ * actually injects it (onSteerAccepted) or it is delivered as a fresh turn
16
+ * (onUserMessage then releases the float).
17
+ */
18
+ onSteerQueued?: (text: string) => void;
19
+ /**
20
+ * A steered message is now part of the conversation (the agent injected it,
21
+ * or the turn ended with it persisted) — release the float and render it as
22
+ * a transcript user message.
23
+ */
24
+ onSteerAccepted?: (text: string) => void;
25
+ onContent: (delta: string, messageId?: string) => void;
26
+ onReasoning: (delta: string) => void;
27
+ onUsage: (usage: AgentUsage) => void;
28
+ onServerTool: (tool: ServerToolEvent) => void;
29
+ onToolStart: (call: ClientToolCall, summary: string) => void;
30
+ onToolEnd: (call: ClientToolCall, output: string, isError: boolean) => void;
31
+ onNotice: (text: string, level: "info" | "error" | "warn", code?: string) => void;
32
+ onRunState: (state: RunState) => void;
33
+ onThread: (threadId: string) => void;
34
+ onSession: (detail: SessionDetail) => void;
35
+ }
36
+ export interface AgentRunnerOptions {
37
+ client: IntegrationClient;
38
+ registry: ToolRegistry;
39
+ workspace: string;
40
+ allowOutside: boolean;
41
+ skillDirs: string[];
42
+ todos: TodoStore;
43
+ approval: ApprovalPolicy;
44
+ /** Compose the (mode-independent) per-request system block. */
45
+ buildSystemPrompt: () => Promise<string>;
46
+ callbacks: TurnCallbacks;
47
+ /** Override the event-channel reconnect delay (tests). */
48
+ eventReconnectDelayMs?: number;
49
+ }
50
+ export declare class AgentRunner {
51
+ private readonly opts;
52
+ threadId: string | null;
53
+ mode: AgentMode;
54
+ /** undefined = omit thinkingLevel (provider default). */
55
+ thinkingLevel: string | undefined;
56
+ /**
57
+ * The model the user picked for the next new thread (null = client-choice
58
+ * default). Sent only on a new thread's first run: the server pins the
59
+ * thread's model on its first run, and a later request naming a different
60
+ * model on the same thread is rejected with 400.
61
+ */
62
+ selectedModel: string | null;
63
+ /**
64
+ * Effective model of the current/resumed thread, as reported by the server.
65
+ * Display only — selection goes through `selectedModel`.
66
+ */
67
+ model: string | null;
68
+ usage: AgentUsage | null;
69
+ state: RunState;
70
+ private abort;
71
+ /**
72
+ * True from stop() until the next run starts — the cancelled-approval
73
+ * wording keys off it (the AbortController is nulled by run()'s finally
74
+ * before an in-flight round resumes, so it cannot be used there).
75
+ */
76
+ private stopped;
77
+ private followUps;
78
+ /**
79
+ * Chain of fire-and-forget follow-up turns (user follow-ups).
80
+ * `waitForIdle()` awaits it so headless runs don't exit before a queued
81
+ * turn has been delivered.
82
+ */
83
+ private followUpChain;
84
+ /**
85
+ * Tool calls already reported as released — populated from the run stream's
86
+ * `TOOL_CALL_RESULT(isError)` echoes, `RUN_FINISHED` with calls still
87
+ * pending, and the fallback event channel. Also the client-side idempotency
88
+ * filter: a replayed CUSTOM_TOOL_CALL for an abandoned call is never
89
+ * executed again.
90
+ */
91
+ private abandonedToolCalls;
92
+ /**
93
+ * Every CUSTOM_TOOL_CALL id received this session. The replay guard: an id
94
+ * seen once is never enqueued again, whatever the delivery path (run stream,
95
+ * event-channel replay) — a duplicate would execute a side-effecting tool
96
+ * twice. Cumulative like abandonedToolCalls: ids are unique per call, so
97
+ * there is nothing to reset.
98
+ */
99
+ private seenToolCallIds;
100
+ /**
101
+ * Unix ms of a server-reported interruption of the previous run (restart /
102
+ * idle scale-down) — wording only: released calls are explained as an
103
+ * interruption instead of an ordinary no-response release.
104
+ */
105
+ private lastRunInterruptedAt;
106
+ /**
107
+ * Calls received from the agent but not yet submitted: the release filter.
108
+ * An id is removed BEFORE its submission request goes out, so the server's
109
+ * echo of our own result can never be misread as a release.
110
+ */
111
+ private pendingSubmission;
112
+ /**
113
+ * Steers acked but not yet injected into the run, keyed by the persisted
114
+ * message id (from INPUT_ACCEPTED). The ack only means "persisted + queued
115
+ * for injection" — the float stays up until QUEUED_MESSAGE_DELIVERED (the
116
+ * agent actually put the message into the conversation) releases it. The
117
+ * turn-end sweep releases whatever is left: unconsumed steers are re-issued
118
+ * as a fresh prompt by the manager, and the message is in the session
119
+ * either way, so the float must not linger.
120
+ */
121
+ private pendingSteerDeliveries;
122
+ /**
123
+ * Tool rounds scheduled or executing (debounce included). The turn drains
124
+ * them before it settles, so a round is never raced by the followUps drain.
125
+ */
126
+ private roundsInFlight;
127
+ private roundsDrained;
128
+ /**
129
+ * True once the current run segment ended (RUN_FINISHED/RUN_ERROR) — a
130
+ * stream that closes without this observed a drop, not a settle.
131
+ */
132
+ private runSettled;
133
+ /**
134
+ * True while a channel-loss notice is outstanding (cleared when events flow
135
+ * again) — keeps "lost the event channel" to one notice per outage.
136
+ */
137
+ private eventChannelDown;
138
+ /** Fallback event-channel subscription (reconcile window only). */
139
+ private eventsAbort;
140
+ /**
141
+ * Mode last announced to the server. null = unknown (new/resumed thread), so
142
+ * the next message carries `[mode: x]`. Kept in sync by the send paths.
143
+ */
144
+ private announcedMode;
145
+ constructor(opts: AgentRunnerOptions);
146
+ get busy(): boolean;
147
+ setMode(mode: AgentMode): void;
148
+ /** Forget the announced mode (new thread or resume) — re-announce on next send. */
149
+ resetModeAnnouncement(): void;
150
+ setThinkingLevel(level: string | undefined): void;
151
+ /** Select the model for the next new thread (null = server default). */
152
+ setModel(model: string | null): void;
153
+ /** Advertised tools are mode-independent: a stable schema keeps the prompt cache warm. */
154
+ private toolDefs;
155
+ /** Tool definitions in AG-UI protocol shape (name/description/parameters). */
156
+ private aguiToolDefs;
157
+ /**
158
+ * The user text to send, prefixed with `[mode: x]` when the mode changed
159
+ * since the last send (or was never announced). Also returns the mode being
160
+ * announced so the caller can record it after a successful send. The marker
161
+ * is the only mode signal the model gets — the system prompt describes both
162
+ * modes statically.
163
+ */
164
+ private announcedText;
165
+ private toolContext;
166
+ /**
167
+ * Compose a send's input messages: the per-request system block (when one is
168
+ * built) plus the user message with its `[mode: x]` marker applied. Shared by
169
+ * the steer and run paths so their mode-announcement semantics cannot drift.
170
+ */
171
+ private composeInput;
172
+ /**
173
+ * Send a user message. While a run is active:
174
+ * - default: steered into the in-flight run (POST .../messages)
175
+ * - `followUp`: queued locally and delivered after the run settles
176
+ */
177
+ send(text: string, opts?: {
178
+ followUp?: boolean;
179
+ }): Promise<void>;
180
+ /** Force-stop the run (used by Esc and /stop). */
181
+ stop(): Promise<void>;
182
+ /** Manually compact the remote session history. */
183
+ compact(): Promise<void>;
184
+ /** Refresh model/usage/thinking metadata from the server. */
185
+ refreshSession(includeHistory?: boolean): Promise<SessionDetail | null>;
186
+ private run;
187
+ /**
188
+ * Wait for every in-flight tool round (or an abort, so /stop never hangs on
189
+ * a pending approval).
190
+ */
191
+ private drainRounds;
192
+ /**
193
+ * Open the run stream and dispatch its events until it closes. Returns when
194
+ * the stream ended; `runSettled` distinguishes a clean settle from a drop.
195
+ */
196
+ private streamRun;
197
+ /**
198
+ * One AG-UI event from the run stream. Terminal events drive release
199
+ * detection and the settle flag; `CUSTOM_TOOL_CALL` events feed the
200
+ * debounced execution round.
201
+ */
202
+ private handleRunEvent;
203
+ private applyUsage;
204
+ /** Report every call still awaiting submission as released (run ended). */
205
+ private releasePending;
206
+ /** Release the float for a steer the agent actually injected (swap to a
207
+ * transcript bubble). Unknown ids (another client's steer) are ignored. */
208
+ private releaseSteer;
209
+ /** Release every float still pending — the turn ended before the agent
210
+ * injected the message (settled, dropped, stopped, errored). The message
211
+ * is persisted in the session either way, so the float must not linger. */
212
+ private releasePendingSteers;
213
+ /**
214
+ * Execute a round's client tool calls (approval + plan-mode refusal) and
215
+ * deliver the results. Runs off the event dispatcher: the run stream stays
216
+ * attached throughout, so release/continuation events keep flowing while a
217
+ * human decides.
218
+ */
219
+ private executeAndSubmit;
220
+ /**
221
+ * Resolve once the runner is idle and every queued follow-up turn has been
222
+ * delivered. Headless runs await this before exiting so a queued turn is
223
+ * never cut off by process exit.
224
+ */
225
+ waitForIdle(): Promise<void>;
226
+ /**
227
+ * Deliver a round's tool results, reconciling against the server's truth
228
+ * first when it advertises `toolCallExpiry`:
229
+ *
230
+ * - still pending (exact id) → submit as-is (native tool result, same run);
231
+ * - the model retried the same action (same name + canonical args) → adopt
232
+ * the retry's id and submit the already-executed result (no double side
233
+ * effect);
234
+ * - other pending calls the client never executed → run them through the
235
+ * normal approval+execution path and submit their results too;
236
+ * - outputs with no matching pending call → the server already released
237
+ * the call; the result is discarded (the agent moved on when the call
238
+ * was released — a follow-up message would only confuse it).
239
+ *
240
+ * A submission that still races a release (pre-stream 400 or the
241
+ * INPUT_REJECTED frame) re-reconciles once.
242
+ */
243
+ private submitRound;
244
+ /**
245
+ * Compare a round's outputs against the server's pending list. Returns the
246
+ * outputs that can still be delivered (exact ids + adopted retries + newly
247
+ * executed pending calls) and the ones whose calls the server already
248
+ * released (discarded by the caller).
249
+ */
250
+ private reconcileRound;
251
+ /**
252
+ * Discard results whose calls the server already released. The agent
253
+ * answered those calls when they were released — delivering the result now
254
+ * (as a follow-up message) would only confuse it. The release itself was
255
+ * usually already surfaced (run stream / channel); only announce the
256
+ * discard when this is the first the user hears of it.
257
+ */
258
+ private discardReleasedResults;
259
+ /** Human reason for a released call, preferring a known server restart. */
260
+ private releaseReason;
261
+ /**
262
+ * Mark calls as released and tell the user once. Only calls this turn is
263
+ * actually waiting on are surfaced (a result for an older call arriving on
264
+ * the stream/channel is not actionable) — `force` is for ids the server
265
+ * itself reported as expired, which have already left the pending set.
266
+ */
267
+ private markAbandoned;
268
+ private latchThread;
269
+ /** Open the session event subscription for the recovery window (capability-gated). */
270
+ private startEventSubscription;
271
+ private stopEventSubscription;
272
+ /**
273
+ * Subscribe → reconnect loop. Bounded (see MAX_EVENT_RECONNECTS) and stopped
274
+ * as soon as the outstanding call is known to be released, so a paused
275
+ * sandbox is woken at most a few times and never kept awake by the channel.
276
+ *
277
+ * Loss and exhaustion are surfaced (once each, state change only): a long
278
+ * approval wait can outlive the sandbox, and the user staring at the
279
+ * approval card deserves to know the channel went away.
280
+ */
281
+ private runEventSubscription;
282
+ /**
283
+ * Handle one fallback-channel event. The channel is subscribed only when no
284
+ * run stream is attached (the recovery window), so an idle snapshot or a
285
+ * terminal run event means the calls still awaiting submission were
286
+ * released. Echoes of our own submissions are already out of
287
+ * `pendingSubmission` and can never be read as releases.
288
+ */
289
+ private handleSessionEvent;
290
+ private parseCall;
291
+ /** Read-only calls run concurrently; mutating and unknown calls serialize. */
292
+ private executeToolCalls;
293
+ /**
294
+ * Execute one client tool call. Returns null when the call was cancelled
295
+ * (server released it, or the run was stopped while the user decided): the
296
+ * tool never ran and nothing may be submitted for it.
297
+ */
298
+ private executeOne;
299
+ /**
300
+ * The run stream died before the run settled. Reconcile against the durable
301
+ * session: if the backend is waiting on tool calls, re-run READ-ONLY ones
302
+ * (safe) and fail mutating ones rather than re-applying side effects
303
+ * silently. The event channel watches for a release while the round is
304
+ * finished (no request stream is attached in this window).
305
+ */
306
+ private reconcile;
307
+ }
308
+ /** Convert persisted session usage into the live-usage shape the UI renders. */
309
+ export declare function usageFromSession(usage: SessionDetail["usage"]): AgentUsage | null;