@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.
- package/README.md +357 -0
- package/dist/agent/context.d.ts +33 -0
- package/dist/agent/context.js +169 -0
- package/dist/agent/modes.d.ts +21 -0
- package/dist/agent/modes.js +84 -0
- package/dist/agent/prompt-builder.d.ts +9 -0
- package/dist/agent/prompt-builder.js +31 -0
- package/dist/agent/sessions.d.ts +38 -0
- package/dist/agent/sessions.js +130 -0
- package/dist/agent/todo.d.ts +18 -0
- package/dist/agent/todo.js +61 -0
- package/dist/agent/turn.d.ts +309 -0
- package/dist/agent/turn.js +1253 -0
- package/dist/approval/policy.d.ts +81 -0
- package/dist/approval/policy.js +157 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +156 -0
- package/dist/git/status.d.ts +89 -0
- package/dist/git/status.js +226 -0
- package/dist/headless.d.ts +72 -0
- package/dist/headless.js +330 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +511 -0
- package/dist/protocol/client.d.ts +123 -0
- package/dist/protocol/client.js +250 -0
- package/dist/protocol/sse-frames.d.ts +6 -0
- package/dist/protocol/sse-frames.js +75 -0
- package/dist/protocol/types.d.ts +200 -0
- package/dist/protocol/types.js +8 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +166 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +21 -0
- package/dist/skills/discovery.d.ts +24 -0
- package/dist/skills/discovery.js +109 -0
- package/dist/tools/binary.d.ts +2 -0
- package/dist/tools/binary.js +22 -0
- package/dist/tools/diff.d.ts +1 -0
- package/dist/tools/diff.js +49 -0
- package/dist/tools/find.d.ts +2 -0
- package/dist/tools/find.js +61 -0
- package/dist/tools/fs.d.ts +2 -0
- package/dist/tools/fs.js +276 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +131 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +228 -0
- package/dist/tools/paths.d.ts +27 -0
- package/dist/tools/paths.js +124 -0
- package/dist/tools/registry.d.ts +13 -0
- package/dist/tools/registry.js +38 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +136 -0
- package/dist/tools/skills.d.ts +2 -0
- package/dist/tools/skills.js +36 -0
- package/dist/tools/todo.d.ts +2 -0
- package/dist/tools/todo.js +43 -0
- package/dist/tools/transfer.d.ts +2 -0
- package/dist/tools/transfer.js +145 -0
- package/dist/tools/truncate.d.ts +12 -0
- package/dist/tools/truncate.js +46 -0
- package/dist/tools/types.d.ts +85 -0
- package/dist/tools/types.js +63 -0
- package/dist/ui/app.d.ts +39 -0
- package/dist/ui/app.js +1061 -0
- package/dist/ui/colors.d.ts +100 -0
- package/dist/ui/colors.js +169 -0
- package/dist/ui/components.d.ts +267 -0
- package/dist/ui/components.js +811 -0
- package/dist/ui/diff.d.ts +37 -0
- package/dist/ui/diff.js +143 -0
- package/dist/ui/format.d.ts +28 -0
- package/dist/ui/format.js +76 -0
- package/dist/ui/help.d.ts +6 -0
- package/dist/ui/help.js +45 -0
- package/dist/ui/highlight.d.ts +20 -0
- package/dist/ui/highlight.js +210 -0
- package/dist/ui/logo.d.ts +24 -0
- package/dist/ui/logo.js +106 -0
- package/dist/ui/model-list.d.ts +10 -0
- package/dist/ui/model-list.js +33 -0
- package/dist/ui/quit-confirm.d.ts +8 -0
- package/dist/ui/quit-confirm.js +40 -0
- package/dist/ui/select-popup.d.ts +30 -0
- package/dist/ui/select-popup.js +54 -0
- package/dist/ui/theme.d.ts +4 -0
- package/dist/ui/theme.js +41 -0
- package/dist/ui/tool-view.d.ts +20 -0
- package/dist/ui/tool-view.js +326 -0
- package/package.json +44 -0
- 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;
|