@code-yeongyu/senpi 2026.10.10-2 → 2026.10.10-4
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 +40 -0
- package/dist/bundle/chunks/{anthropic-messages-CVXI6CLT.js → anthropic-messages-OE3QBX4P.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-WZG7WYMD.js → app-server-command-N3L2LNP7.js} +1 -1
- package/dist/bundle/chunks/{chunk-Q2ZTPMWD.js → chunk-4TK7VCZZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SB22X6ZR.js → chunk-H4UIUFU5.js} +1 -1
- package/dist/bundle/chunks/chunk-I5HLTDT4.js +2 -0
- package/dist/bundle/chunks/{chunk-W6B7SEAK.js → chunk-MP2DMTI7.js} +48 -46
- package/dist/bundle/chunks/{chunk-LSE7KE2C.js → chunk-ODYQK5XJ.js} +1 -1
- package/dist/bundle/chunks/chunk-USRAHAW4.js +14 -0
- package/dist/bundle/chunks/{chunk-3YXB7Z7O.js → chunk-Z7MMKKIY.js} +1 -1
- package/dist/bundle/chunks/{chunk-CTERKXVZ.js → chunk-ZSYRWQWG.js} +2 -2
- package/dist/bundle/chunks/{cli-main-T4T5UNRJ.js → cli-main-CHDR33FZ.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-44VUGB7H.js → help-fast-path-EHRZEBRO.js} +1 -1
- package/dist/bundle/chunks/{host-command-X64D7PT3.js → host-command-VATE7HRG.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-ULFSI46Z.js → interactive-mode-RVEM62WX.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-VM7WJBC5.js → multi-session-host-CFND7CXH.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-6HU7GIGD.js → openai-completions-Q2LEWXEC.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-S4QYZXF3.js → openai-responses-DHGWSTZM.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-OAFUQY7E.js → package-manager-cli-NWNECBF7.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-ZJQ6I2RZ.js → rotation-stream-C5BM7TMM.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-XVRJJG3K.js → rpc-mode-JQFUDLNB.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-HMB5T5VD.js +3 -0
- package/dist/bundle/chunks/{session-picker-R5N2EKUS.js → session-picker-O7OESJWU.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +67 -65
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.js +18 -4
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +3 -1
- package/dist/core/extensions/builtin/goal/prompt.d.ts +6 -1
- package/dist/core/extensions/builtin/goal/prompt.js +32 -1
- package/dist/core/extensions/builtin/websearch/websearch/config.js +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.js +1 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.d.ts +3 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.js +55 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers.js +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +1 -1
- package/dist/core/extensions/session-control-types.d.ts +25 -0
- package/dist/core/extensions/session-control-types.js +16 -0
- package/dist/core/external-admission.js +7 -1
- package/dist/core/model-config-schema.d.ts +5 -0
- package/dist/core/model-config-schema.js +1 -0
- package/dist/core/provider-composer.js +5 -1
- package/dist/core/provider-timeout-retry.d.ts +7 -1
- package/dist/core/provider-timeout-retry.js +23 -12
- package/dist/core/reload-veto.d.ts +1 -1
- package/dist/core/reload-veto.js +7 -1
- package/dist/core/resource-loader.js +63 -2
- package/dist/core/session-rebind.js +16 -1
- package/dist/modes/interactive/components/extension-editor.js +4 -0
- package/dist/modes/interactive/components/remote-delivery-message.d.ts +6 -3
- package/dist/modes/interactive/components/remote-delivery-message.js +32 -3
- package/dist/modes/interactive/interactive-mode.d.ts +17 -0
- package/dist/modes/interactive/interactive-mode.js +58 -29
- package/dist/modes/interactive/interactive-stderr-guard.d.ts +8 -0
- package/dist/modes/interactive/interactive-stderr-guard.js +70 -0
- package/dist/modes/interactive/session-control-commands.d.ts +2 -1
- package/dist/modes/interactive/session-control-commands.js +21 -7
- package/dist/modes/interactive/session-control-session-commands.d.ts +22 -0
- package/dist/modes/interactive/session-control-session-commands.js +47 -0
- package/dist/modes/interactive/stdout-fd-redirect.d.ts +12 -0
- package/dist/modes/interactive/stdout-fd-redirect.js +114 -0
- package/dist/modes/rpc/connection-handler.js +6 -15
- package/dist/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/modes/rpc/session-control-actions-data.d.ts +17 -0
- package/dist/modes/rpc/session-control-actions-data.js +38 -0
- package/docs/extensions.md +1 -1
- package/docs/models.md +34 -0
- package/docs/rpc.md +38 -4
- package/docs/web-search.md +3 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-ULYOMGXX.js +0 -14
- package/dist/bundle/chunks/chunk-YSPHACIN.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-FEGOYWHN.js +0 -3
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal control endpoint's session controls: a model switch, a thinking level and an interrupt.
|
|
3
|
+
* Each one runs the pane's own path (`/model`, the thinking-level selector, Esc) through the surface,
|
|
4
|
+
* so validation, the footer and persistence are the ones the user gets typing it there.
|
|
5
|
+
*/
|
|
6
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
7
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
8
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
9
|
+
import { type ControlCommand } from "./session-control-server.ts";
|
|
10
|
+
export interface TuiSessionControls {
|
|
11
|
+
/** The `/model` path: switches, updates the footer and records the default; throws a refusal. */
|
|
12
|
+
selectModel(model: Model<string>): Promise<{
|
|
13
|
+
readonly systemPromptName?: string;
|
|
14
|
+
} | undefined>;
|
|
15
|
+
/** The thinking-level selector: `remember` is its Ctrl+S (the model's remembered level). */
|
|
16
|
+
selectThinkingLevel(level: ThinkingLevel, remember: boolean): void;
|
|
17
|
+
/** Esc on a running turn: queued input returns to the editor, the turn aborts and settles. */
|
|
18
|
+
interruptTurn(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare const SESSION_CONTROL_COMMANDS: readonly ["get_available_models", "get_available_thinking_levels", "set_model", "set_thinking_level", "interrupt"];
|
|
21
|
+
export declare function runSessionControlCommand(session: AgentSession, controls: TuiSessionControls, command: ControlCommand): Promise<object | undefined>;
|
|
22
|
+
//# sourceMappingURL=session-control-session-commands.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { availableModelsData, interruptRunningTurn, unsupportedThinkingLevel, } from "../rpc/session-control-actions-data.js";
|
|
2
|
+
import { failure, success } from "./session-control-server.js";
|
|
3
|
+
export const SESSION_CONTROL_COMMANDS = [
|
|
4
|
+
"get_available_models",
|
|
5
|
+
"get_available_thinking_levels",
|
|
6
|
+
"set_model",
|
|
7
|
+
"set_thinking_level",
|
|
8
|
+
"interrupt",
|
|
9
|
+
];
|
|
10
|
+
export async function runSessionControlCommand(session, controls, command) {
|
|
11
|
+
const { id, type } = command;
|
|
12
|
+
switch (type) {
|
|
13
|
+
case "get_available_models":
|
|
14
|
+
return success(id, type, await availableModelsData(session));
|
|
15
|
+
case "get_available_thinking_levels":
|
|
16
|
+
return success(id, type, { levels: session.getAvailableThinkingLevels() });
|
|
17
|
+
case "set_model":
|
|
18
|
+
return await setModel(session, controls, command);
|
|
19
|
+
case "set_thinking_level": {
|
|
20
|
+
const level = session.getAvailableThinkingLevels().find((candidate) => candidate === command.level);
|
|
21
|
+
if (level === undefined)
|
|
22
|
+
return failure(id, type, unsupportedThinkingLevel(command.level));
|
|
23
|
+
controls.selectThinkingLevel(level, command.scope !== "turn");
|
|
24
|
+
return success(id, type);
|
|
25
|
+
}
|
|
26
|
+
case "interrupt":
|
|
27
|
+
return success(id, type, await interruptRunningTurn(session, command.turnId, () => controls.interruptTurn()));
|
|
28
|
+
default:
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function setModel(session, controls, command) {
|
|
33
|
+
const { id, type } = command;
|
|
34
|
+
const models = await session.modelRegistry.getAvailable();
|
|
35
|
+
const model = models.find((candidate) => candidate.provider === command.provider && candidate.id === command.modelId);
|
|
36
|
+
if (model === undefined) {
|
|
37
|
+
return failure(id, type, `Model not found: ${String(command.provider)}/${String(command.modelId)}`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const change = await controls.selectModel(model);
|
|
41
|
+
return success(id, type, { ...model, systemPromptName: change?.systemPromptName });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return failure(id, type, error instanceof Error ? error.message : String(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=session-control-session-commands.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StderrFdSyscalls } from "./stderr-fd-redirect.ts";
|
|
2
|
+
type StdoutLike = NodeJS.WriteStream;
|
|
3
|
+
export interface StdoutFdRedirect {
|
|
4
|
+
restore(): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Point fd 1 at `logPath` (append, 0600) and re-point `stdout` at a duplicate of the terminal.
|
|
8
|
+
* Returns `undefined`, leaving fd 1 and `stdout` untouched, when any step fails.
|
|
9
|
+
*/
|
|
10
|
+
export declare function redirectStdoutFd(syscalls: StderrFdSyscalls, logPath: string, stdout?: StdoutLike): StdoutFdRedirect | undefined;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=stdout-fd-redirect.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-descriptor-level stdout capture for the interactive TUI (#2815).
|
|
3
|
+
*
|
|
4
|
+
* The TUI draws on fd 1. Anything else that writes fd 1 directly while the TUI owns the screen (a
|
|
5
|
+
* `writeSync(1, ...)`, a child spawned with `stdio: "inherit"`, Bun's native `console.log`, a Worker's
|
|
6
|
+
* console) scrolls the real terminal behind the renderer's back. The renderer's cursor math then goes
|
|
7
|
+
* stale and the editor is pushed below the visible screen. Mirroring the fd 2 capture
|
|
8
|
+
* (`stderr-fd-redirect.ts`, #2284): while the TUI owns the screen, fd 1 is pointed at the debug log
|
|
9
|
+
* with `dup2`, and `process.stdout` (the renderer's only writer) is re-pointed at a duplicate of the
|
|
10
|
+
* terminal descriptor, so its writes, size and resize events keep coming from the terminal itself.
|
|
11
|
+
* The original fd 1 is put back before the terminal is released.
|
|
12
|
+
*
|
|
13
|
+
* Only the interactive TUI installs this. Print, JSON and RPC modes use stdout as their protocol and
|
|
14
|
+
* never call it. Windows has no `dup`/`dup2` here (`loadStderrFdSyscalls` returns undefined), so
|
|
15
|
+
* the JS-level guard stays the only protection there.
|
|
16
|
+
*/
|
|
17
|
+
import { closeSync, createWriteStream, openSync } from "node:fs";
|
|
18
|
+
import { isatty, WriteStream as TtyWriteStream } from "node:tty";
|
|
19
|
+
const STDOUT_FD = 1;
|
|
20
|
+
// One duplicate of the terminal per process: every takeover after a suspend or an external editor
|
|
21
|
+
// reuses it, so a long session does not accumulate descriptors.
|
|
22
|
+
let terminal;
|
|
23
|
+
// Our `write` wrappers, and the `write` that was in place before the first of them. A release can leave
|
|
24
|
+
// an inactive wrapper installed (the renderer's own guard is removed after it); the next takeover wraps
|
|
25
|
+
// the base write instead, so suspend/resume cycles never stack wrappers.
|
|
26
|
+
const ownWrites = new WeakSet();
|
|
27
|
+
let baseWrite;
|
|
28
|
+
function terminalHandle(syscalls) {
|
|
29
|
+
if (terminal)
|
|
30
|
+
return terminal;
|
|
31
|
+
const fd = syscalls.dup(STDOUT_FD);
|
|
32
|
+
if (fd < 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const stream = isatty(fd) ? new TtyWriteStream(fd) : createWriteStream("", { fd, autoClose: false });
|
|
35
|
+
terminal = { fd, stream };
|
|
36
|
+
return terminal;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Point fd 1 at `logPath` (append, 0600) and re-point `stdout` at a duplicate of the terminal.
|
|
40
|
+
* Returns `undefined`, leaving fd 1 and `stdout` untouched, when any step fails.
|
|
41
|
+
*/
|
|
42
|
+
export function redirectStdoutFd(syscalls, logPath, stdout = process.stdout) {
|
|
43
|
+
const handle = terminalHandle(syscalls);
|
|
44
|
+
if (!handle)
|
|
45
|
+
return undefined;
|
|
46
|
+
let target;
|
|
47
|
+
try {
|
|
48
|
+
target = openSync(logPath, "a", 0o600);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const moved = syscalls.dup2(target, STDOUT_FD);
|
|
54
|
+
closeSync(target);
|
|
55
|
+
if (moved < 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
return installTerminalWriter(syscalls, handle, stdout);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Never leave fd 1 pointed at the log without a way back.
|
|
62
|
+
syscalls.dup2(handle.fd, STDOUT_FD);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function installTerminalWriter(syscalls, handle, stdout) {
|
|
67
|
+
let active = true;
|
|
68
|
+
if (!ownWrites.has(stdout.write))
|
|
69
|
+
baseWrite = stdout.write;
|
|
70
|
+
const originalWrite = baseWrite ?? stdout.write;
|
|
71
|
+
const write = function (...args) {
|
|
72
|
+
if (!active)
|
|
73
|
+
return originalWrite.apply(stdout, args);
|
|
74
|
+
return handle.stream.write.apply(handle.stream, args);
|
|
75
|
+
};
|
|
76
|
+
ownWrites.add(write);
|
|
77
|
+
stdout.write = write;
|
|
78
|
+
// fd 1 is a file now, so the stream's own size reads would come back empty: read the terminal's.
|
|
79
|
+
const sizeProperties = ["columns", "rows"];
|
|
80
|
+
const ownDescriptors = new Map(sizeProperties.map((name) => [name, Object.getOwnPropertyDescriptor(stdout, name)]));
|
|
81
|
+
for (const name of sizeProperties) {
|
|
82
|
+
Object.defineProperty(stdout, name, {
|
|
83
|
+
configurable: true,
|
|
84
|
+
enumerable: true,
|
|
85
|
+
get: () => handle.stream[name],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const onWindowChange = () => {
|
|
89
|
+
handle.stream._refreshSize?.();
|
|
90
|
+
stdout.emit("resize");
|
|
91
|
+
};
|
|
92
|
+
process.on("SIGWINCH", onWindowChange);
|
|
93
|
+
return {
|
|
94
|
+
restore() {
|
|
95
|
+
if (!active)
|
|
96
|
+
return;
|
|
97
|
+
active = false;
|
|
98
|
+
process.removeListener("SIGWINCH", onWindowChange);
|
|
99
|
+
syscalls.dup2(handle.fd, STDOUT_FD);
|
|
100
|
+
// A wrapper installed on top of ours (the renderer's own guard) keeps calling `write`, which
|
|
101
|
+
// now forwards to the original stdout on the restored fd 1.
|
|
102
|
+
if (stdout.write === write)
|
|
103
|
+
stdout.write = originalWrite;
|
|
104
|
+
for (const name of sizeProperties) {
|
|
105
|
+
const descriptor = ownDescriptors.get(name);
|
|
106
|
+
if (descriptor)
|
|
107
|
+
Object.defineProperty(stdout, name, descriptor);
|
|
108
|
+
else
|
|
109
|
+
delete stdout[name];
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=stdout-fd-redirect.js.map
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as crypto from "node:crypto";
|
|
18
18
|
import { basename, dirname, extname } from "node:path";
|
|
19
|
-
import { modelSupportsAssistantPrefill } from "@earendil-works/pi-ai";
|
|
20
19
|
import { VERSION } from "../../config.js";
|
|
21
20
|
import { authMethodStatus, buildLoginProviderInfos } from "../../core/auth-providers.js";
|
|
22
21
|
import { ContinueFromLeafError } from "../../core/continue-from-leaf.js";
|
|
@@ -27,7 +26,6 @@ import { emitProviderAccountsChanged, subscribeProviderAccountEvents, } from "..
|
|
|
27
26
|
import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "../../core/extensions/builtin/anthropic-subscription/account-management.js";
|
|
28
27
|
import { isMcpControlInventoryChanged, MCP_CONTROL_INVENTORY_CHANGED_EVENT, MCP_CONTROL_INVENTORY_REQUEST_EVENT, } from "../../core/extensions/builtin/mcp/control-inventory.js";
|
|
29
28
|
import { applyFastMode, resolveServiceTierMemoryModel, } from "../../core/extensions/builtin/service-tier.js";
|
|
30
|
-
import { getSupportedThinkingLevels } from "../../core/thinking-levels.js";
|
|
31
29
|
import { theme } from "../interactive/theme/theme.js";
|
|
32
30
|
import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
|
|
33
31
|
import { ClientAdmissions } from "./client-admissions.js";
|
|
@@ -45,6 +43,7 @@ import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListD
|
|
|
45
43
|
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
46
44
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
47
45
|
import { RPC_ERROR_MEDIA_NOT_FOUND } from "./rpc-types.js";
|
|
46
|
+
import { availableModelsData, interruptRunningTurn, unsupportedThinkingLevel } from "./session-control-actions-data.js";
|
|
48
47
|
import { SessionExtensionUiRequests } from "./session-extension-ui-requests.js";
|
|
49
48
|
export { buildRpcSessionState } from "./rpc-session-state.js";
|
|
50
49
|
/** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
|
|
@@ -809,6 +808,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
809
808
|
});
|
|
810
809
|
return success(id, "abort");
|
|
811
810
|
}
|
|
811
|
+
case "interrupt":
|
|
812
|
+
return success(id, "interrupt", await interruptRunningTurn(session, command.turnId, () => session.abort()));
|
|
812
813
|
case "abort_compaction": {
|
|
813
814
|
session.abortCompaction();
|
|
814
815
|
return success(id, "abort_compaction");
|
|
@@ -891,18 +892,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
891
892
|
}
|
|
892
893
|
return success(id, "cycle_model", result);
|
|
893
894
|
}
|
|
894
|
-
case "get_available_models":
|
|
895
|
-
|
|
896
|
-
return success(id, "get_available_models", {
|
|
897
|
-
models: models.map((model) => ({
|
|
898
|
-
...model,
|
|
899
|
-
supportedThinkingLevels: getSupportedThinkingLevels(model),
|
|
900
|
-
supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
|
|
901
|
-
thinkingEnabled: session.thinkingLevel !== "off",
|
|
902
|
-
}),
|
|
903
|
-
})),
|
|
904
|
-
});
|
|
905
|
-
}
|
|
895
|
+
case "get_available_models":
|
|
896
|
+
return success(id, "get_available_models", await availableModelsData(session));
|
|
906
897
|
// =================================================================
|
|
907
898
|
// Thinking
|
|
908
899
|
// =================================================================
|
|
@@ -912,7 +903,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
912
903
|
// neighbour, so applying first would leave a REJECTED request's clamped level in
|
|
913
904
|
// place. A failed command must not change session state.
|
|
914
905
|
if (!session.getAvailableThinkingLevels().includes(command.level)) {
|
|
915
|
-
return error(id, "set_thinking_level",
|
|
906
|
+
return error(id, "set_thinking_level", unsupportedThinkingLevel(command.level));
|
|
916
907
|
}
|
|
917
908
|
session.setSessionThinkingLevel(command.level);
|
|
918
909
|
}
|
|
@@ -73,6 +73,10 @@ type RpcSessionCommand = {
|
|
|
73
73
|
} & ClientMessageIdentity) | {
|
|
74
74
|
id?: string;
|
|
75
75
|
type: "abort";
|
|
76
|
+
} | {
|
|
77
|
+
id?: string;
|
|
78
|
+
type: "interrupt";
|
|
79
|
+
turnId?: string;
|
|
76
80
|
} | {
|
|
77
81
|
id?: string;
|
|
78
82
|
type: "abort_compaction";
|
|
@@ -832,6 +836,15 @@ export type RpcResponse = {
|
|
|
832
836
|
type: "response";
|
|
833
837
|
command: "abort";
|
|
834
838
|
success: true;
|
|
839
|
+
} | {
|
|
840
|
+
id?: string;
|
|
841
|
+
type: "response";
|
|
842
|
+
command: "interrupt";
|
|
843
|
+
success: true;
|
|
844
|
+
data: {
|
|
845
|
+
interrupted: boolean;
|
|
846
|
+
turnId?: string;
|
|
847
|
+
};
|
|
835
848
|
} | {
|
|
836
849
|
id?: string;
|
|
837
850
|
type: "response";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
|
+
export declare function availableModelsData(session: AgentSession): Promise<{
|
|
3
|
+
models: readonly object[];
|
|
4
|
+
}>;
|
|
5
|
+
/** The refusal of a level the active model cannot run; callers match this text. */
|
|
6
|
+
export declare function unsupportedThinkingLevel(level: unknown): string;
|
|
7
|
+
export interface InterruptOutcome {
|
|
8
|
+
readonly interrupted: boolean;
|
|
9
|
+
readonly turnId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `interrupt { turnId? }`: stops the running turn and only that. An idle session answers
|
|
13
|
+
* `interrupted: false`; so does a `turnId` naming a turn that is no longer the running one, which
|
|
14
|
+
* then answers the running turn's id. `stop` resolves once the turn has stopped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function interruptRunningTurn(session: AgentSession, requestedTurnId: unknown, stop: () => Promise<void>): Promise<InterruptOutcome>;
|
|
17
|
+
//# sourceMappingURL=session-control-actions-data.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-control answers a multi-session host and a terminal control endpoint share, so a caller
|
|
3
|
+
* reads the same shapes from either: the model catalog, the thinking-level refusal, and the
|
|
4
|
+
* `interrupt` outcome.
|
|
5
|
+
*/
|
|
6
|
+
import { modelSupportsAssistantPrefill } from "@earendil-works/pi-ai";
|
|
7
|
+
import { getSupportedThinkingLevels } from "../../core/thinking-levels.js";
|
|
8
|
+
export async function availableModelsData(session) {
|
|
9
|
+
const models = await session.modelRegistry.modelRuntime.getAvailable();
|
|
10
|
+
return {
|
|
11
|
+
models: models.map((model) => ({
|
|
12
|
+
...model,
|
|
13
|
+
supportedThinkingLevels: getSupportedThinkingLevels(model),
|
|
14
|
+
supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
|
|
15
|
+
thinkingEnabled: session.thinkingLevel !== "off",
|
|
16
|
+
}),
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The refusal of a level the active model cannot run; callers match this text. */
|
|
21
|
+
export function unsupportedThinkingLevel(level) {
|
|
22
|
+
return `Thinking level ${String(level)} is not supported by the active model.`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `interrupt { turnId? }`: stops the running turn and only that. An idle session answers
|
|
26
|
+
* `interrupted: false`; so does a `turnId` naming a turn that is no longer the running one, which
|
|
27
|
+
* then answers the running turn's id. `stop` resolves once the turn has stopped.
|
|
28
|
+
*/
|
|
29
|
+
export async function interruptRunningTurn(session, requestedTurnId, stop) {
|
|
30
|
+
if (!session.isStreaming && session.retryAttempt === 0)
|
|
31
|
+
return { interrupted: false };
|
|
32
|
+
const turnId = String(session.externalAdmission.turnEpoch);
|
|
33
|
+
if (typeof requestedTurnId === "string" && requestedTurnId !== turnId)
|
|
34
|
+
return { interrupted: false, turnId };
|
|
35
|
+
await stop();
|
|
36
|
+
return { interrupted: true, turnId };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=session-control-actions-data.js.map
|
package/docs/extensions.md
CHANGED
|
@@ -1684,7 +1684,7 @@ runtime's steering or follow-up queue like the user's queued input. The protocol
|
|
|
1684
1684
|
|---|---|---|
|
|
1685
1685
|
| `registerControlEndpoint({ inboxDir, drain, isSessionReferenced? })` | `Promise<SessionControlRegistration>` | POSIX interactive session: binds the `tui` control endpoint and answers `{ status: "registered", socket, dispose }`. On a multi-session host with a public socket it binds nothing and `socket` is the host's. Otherwise `{ status: "unsupported", reason: "unsupported_platform" \| "unsupported_mode" }`, or `{ status: "failed", reason }` when a registration step failed. `drain(event)` runs on every [`session_control_wake`](#session_control_wake) edge and answers `{ admitted: [{ delivery_id, kind }] }`. `inboxDir` is created 0700 and its entry creations and deletions wake the drain. At clean exit a header-only session file is removed unless `isSessionReferenced()` answers `true`. |
|
|
1686
1686
|
| `admissionGate()` | `{ can_admit, hold_reason?, editor_revision, turn_epoch }` | Read-only pre-check; `hold_reason` is `draft` while the user composes or their submission is still on its way into the runtime. |
|
|
1687
|
-
| `admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id? })` | `{ kind, turn_epoch }` | Decides and acts in one synchronous call. `kind` is `started`, `queued`, `steered`, `turn_conflict` (stale epoch, or a steer without one), `held_draft` (nothing enqueued; retry on the next wake) or `already_admitted` (also for a delivery whose entry the file refused, until the run that refused it has settled and the file's last write succeeded). The delivery becomes a `custom` entry `session_control_delivery` whose `details.delivery_id` proves it was applied. Throws once a host started handing the session over (`release_session`). |
|
|
1687
|
+
| `admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id?, sender?, display_text? })` | `{ kind, turn_epoch }` | Decides and acts in one synchronous call. `kind` is `started`, `queued`, `steered`, `turn_conflict` (stale epoch, or a steer without one), `held_draft` (nothing enqueued; retry on the next wake) or `already_admitted` (also for a delivery whose entry the file refused, until the run that refused it has settled and the file's last write succeeded). The delivery becomes a `custom` entry `session_control_delivery` whose `details.delivery_id` proves it was applied. Throws once a host started handing the session over (`release_session`). `sender` (`{ kind: "agent", session_id, name? }`, `{ kind: "command_line", user? }` or `{ kind: "external", platform, author? }`) and `display_text` (the message as written) go to the entry's `details`: the terminal labels the message by its sender over `display_text`, while the model reads `text`. |
|
|
1688
1688
|
| `listAdmittedDeliveries()` | `{ pending, emitted, failed? }` | The process-lifetime ledger: `pending` is held by the runtime, `emitted` has its transcript entry written, `failed` (present only when non-empty, `[{ delivery_id, error }]`) had its entry refused by the session file: no longer held, still with its sender, admissible again once the run that refused it has settled and the file's last write succeeded. |
|
|
1689
1689
|
| `persistHeaderNow()` | `Promise<void>` | Writes the session header now, so the session id is durable before anything exposes it. |
|
|
1690
1690
|
|
package/docs/models.md
CHANGED
|
@@ -142,6 +142,10 @@ Set `api` at provider level (default for all models) or model level (override pe
|
|
|
142
142
|
| `authHeader` | Set `true` to add `Authorization: Bearer <apiKey>` automatically |
|
|
143
143
|
| `models` | Array of model configurations |
|
|
144
144
|
| `modelOverrides` | Per-model overrides for built-in or extension-registered models on this provider |
|
|
145
|
+
| `whitelist` | Model ids to keep; every other model of the provider is removed (see [Hiding Providers and Models](#hiding-providers-and-models)) |
|
|
146
|
+
| `blacklist` | Model ids to remove from the provider |
|
|
147
|
+
| `hideFreeModels` | Set `true` to remove every model of the provider whose input and output cost are both `0` |
|
|
148
|
+
| `disabled` | Set `true` to remove the whole provider |
|
|
145
149
|
|
|
146
150
|
For providers with `models`, non-built-in provider configs need `baseUrl` and an `api` value at either provider or model level. `apiKey` is not required to load the file: models become available when auth is configured through `/login`/`auth.json`, CLI `--api-key`, or provider `apiKey`. If no auth is configured, the models load but stay unavailable in `/model` and `--list-models`.
|
|
147
151
|
|
|
@@ -510,6 +514,36 @@ Merge semantics:
|
|
|
510
514
|
- If a custom model `id` matches a built-in model `id`, the custom model replaces that built-in model.
|
|
511
515
|
- If a custom model `id` is new, it is added alongside built-in models.
|
|
512
516
|
|
|
517
|
+
## Hiding Providers and Models
|
|
518
|
+
|
|
519
|
+
Hidden models and providers disappear from the catalog everywhere it is read: `--list-models`, `/model`, Ctrl+P cycling, startup selection, and `enabledModels` / `favoriteModels` resolution.
|
|
520
|
+
|
|
521
|
+
```json
|
|
522
|
+
{
|
|
523
|
+
"disabledProviders": ["openrouter"],
|
|
524
|
+
"providers": {
|
|
525
|
+
"anthropic": { "whitelist": ["claude-sonnet-4-5"] },
|
|
526
|
+
"openai": { "blacklist": ["gpt-5.4"] },
|
|
527
|
+
"opencode": { "hideFreeModels": true }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
| Key | Where | Effect |
|
|
533
|
+
|-----|-------|--------|
|
|
534
|
+
| `disabledProviders` | Top level, array of provider ids | Removes each listed provider and all of its models. `providers.<id>.disabled: true` does the same for one provider. |
|
|
535
|
+
| `whitelist` | Provider block, array of model ids | Keeps only the listed ids. |
|
|
536
|
+
| `blacklist` | Provider block, array of model ids | Removes the listed ids. |
|
|
537
|
+
| `hideFreeModels` | Provider block, boolean | Removes every model of that provider whose `cost.input` and `cost.output` are both `0`. |
|
|
538
|
+
|
|
539
|
+
Behavior notes:
|
|
540
|
+
- `whitelist` and `blacklist` match whole model ids exactly and case-sensitively; there are no globs. When both are set, a model must be in `whitelist` and not in `blacklist`.
|
|
541
|
+
- All three provider filters run after custom `models` are merged in, and before `modelOverrides`. They also apply to the provider's custom `models` entries.
|
|
542
|
+
- `hideFreeModels` is per provider. Other providers are unaffected, so a local provider whose models cost `0` (Ollama, LM Studio, vLLM) stays listed unless that provider sets the flag itself.
|
|
543
|
+
- `hideFreeModels` also hides a custom `models` entry that omits `cost`, because an omitted `cost` is `0`. Give such a model a nonzero `cost`, or leave the flag off for that provider.
|
|
544
|
+
- `hideFreeModels` reads the catalog `cost` (or the custom model's own `cost`). A `cost` set in `modelOverrides` is not considered.
|
|
545
|
+
- A provider block that sets only `whitelist`, `blacklist`, or `hideFreeModels` is valid; no `baseUrl` or `models` is needed.
|
|
546
|
+
|
|
513
547
|
## Per-model Overrides
|
|
514
548
|
|
|
515
549
|
Use `modelOverrides` to customize built-in models and matching extension-registered models without replacing the provider's full model list.
|
package/docs/rpc.md
CHANGED
|
@@ -1147,10 +1147,11 @@ registrant opens no socket and writes no registry directory.
|
|
|
1147
1147
|
`endpoint_kind: "tui"`, `owner` and `alive`, probing it for at most 1.5 s. `senpi host gc` reaps a dead one on
|
|
1148
1148
|
the same evidence as a host. `ensure`, `handoff` and `stop` refuse it with `unsupported_endpoint_kind` (exit 3)
|
|
1149
1149
|
before connecting, because a terminal endpoint is owned by its terminal process.
|
|
1150
|
-
- **Commands.**
|
|
1151
|
-
|
|
1152
|
-
- `get_protocol_info`: `mode: "tui"`, `capabilities: ["tui_control"]`, `generation: 0
|
|
1153
|
-
`instanceId
|
|
1150
|
+
- **Commands.** Anything not listed below is answered `unsupported` as data. No command can prompt, steer, queue a
|
|
1151
|
+
follow-up, open a session or run a command.
|
|
1152
|
+
- `get_protocol_info`: `mode: "tui"`, `capabilities: ["tui_control"]`, `generation: 0`, the endpoint's
|
|
1153
|
+
`instanceId`, and `commands`: every command this endpoint accepts. A terminal from before the session
|
|
1154
|
+
controls below lists no `commands`.
|
|
1154
1155
|
- `list_sessions`: exactly one row, `kind: "interactive"`, `surface: "tui"`, `attachments: 1`.
|
|
1155
1156
|
- `get_state`: the RPC session state plus `turn_epoch`, `blocking_question`, `compacting`,
|
|
1156
1157
|
`editor_has_draft` and `state_version`.
|
|
@@ -1166,6 +1167,21 @@ registrant opens no socket and writes no registry directory.
|
|
|
1166
1167
|
question (a pending question id from the `question` feed); without it, `id` does. The answer settles by
|
|
1167
1168
|
the host's rule (see `question`) and the reply carries the frame's `id`, as on a host (see "Extension
|
|
1168
1169
|
UI Responses").
|
|
1170
|
+
- The session controls run the pane's own paths, so validation, the footer and persistence are the ones the
|
|
1171
|
+
user gets doing it in that terminal:
|
|
1172
|
+
- `get_available_models` and `get_available_thinking_levels`: the same answers as a host's.
|
|
1173
|
+
- `set_model { provider, modelId }`: the `/model` switch (the footer, the remembered default model, the
|
|
1174
|
+
pane's status line). An unknown model is refused `Model not found: <provider>/<modelId>`; a switch the
|
|
1175
|
+
session refuses answers its reason. Success answers the model, as on a host.
|
|
1176
|
+
- `set_thinking_level { level, scope? }`: the thinking-level selector. `scope: "turn"` sets this session's
|
|
1177
|
+
level; otherwise the level is also remembered for the model (the selector's Ctrl+S). A level the active
|
|
1178
|
+
model cannot run is refused `Thinking level <level> is not supported by the active model.` and nothing
|
|
1179
|
+
changes, for either scope. A host refuses it only for `scope: "turn"`; for the default scope a host clamps it
|
|
1180
|
+
to a level the model runs.
|
|
1181
|
+
- `interrupt { turnId? }`: Esc on a running turn - queued input returns to the editor, the turn aborts and
|
|
1182
|
+
settles before the answer. Answers `{ interrupted: true, turnId }` with the `turn_epoch` it stopped. An idle
|
|
1183
|
+
session answers `{ interrupted: false }`; a `turnId` that is not the running turn answers
|
|
1184
|
+
`{ interrupted: false, turnId }` with the running one and leaves it alone. The session stays open.
|
|
1169
1185
|
- `prompt`, `steer` and `follow_up` are `unsupported`.
|
|
1170
1186
|
- **Admission.** A message from another session enters only through the registrant's drain, which calls
|
|
1171
1187
|
`pi.session.admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id? })`. One synchronous call
|
|
@@ -1726,6 +1742,24 @@ Response:
|
|
|
1726
1742
|
{"type": "response", "command": "abort", "success": true}
|
|
1727
1743
|
```
|
|
1728
1744
|
|
|
1745
|
+
|
|
1746
|
+
#### interrupt
|
|
1747
|
+
|
|
1748
|
+
Stop the running turn and only that one, with the terminal control endpoint's contract (see "Interactive sessions
|
|
1749
|
+
expose a control endpoint"). `turnId` (the `turn_epoch` of `get_state`) is optional.
|
|
1750
|
+
|
|
1751
|
+
```json
|
|
1752
|
+
{"type": "interrupt", "turnId": "3"}
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
Response: `{ interrupted: true, turnId }` once the turn has stopped; `{ interrupted: false }` when the session is
|
|
1756
|
+
idle; `{ interrupted: false, turnId }` (the running turn) when `turnId` names another turn. Unlike `abort`, it
|
|
1757
|
+
answers after the turn settled.
|
|
1758
|
+
|
|
1759
|
+
```json
|
|
1760
|
+
{"type": "response", "command": "interrupt", "success": true, "data": {"interrupted": true, "turnId": "3"}}
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1729
1763
|
#### clear_queue
|
|
1730
1764
|
|
|
1731
1765
|
Remove queued steering and follow-up messages and return their text.
|
package/docs/web-search.md
CHANGED
|
@@ -75,7 +75,9 @@ A config file replaces the free default completely: senpi uses only the provider
|
|
|
75
75
|
| `nativeModel` | The model native search runs on; see [Choosing the model native search runs on](#choosing-the-model-native-search-runs-on). |
|
|
76
76
|
| `providers[]` | The providers to use. Each entry takes `provider`, and optionally `id`, `apiKey`, `baseUrl`, `maxResults`, `timeoutMs`, `priority`, `weight`, `allowedDomains` or `blockedDomains`. |
|
|
77
77
|
|
|
78
|
-
Free engines you can list without a key: `duckduckgo-html`, `exa-mcp`, `startpage`, `mojeek`, `ecosia`, `google-html`, and `
|
|
78
|
+
Free engines you can list without a key: `duckduckgo-html`, `exa-mcp`, `startpage`, `mojeek`, `ecosia`, `google-html`, `searxng`, and `keenable`. Providers that need `apiKey`: `exa`, `tavily`, `brave`, `serper`, `serpdive`, `kagi`, `perplexity`, `z-ai`, `xai`, `kimi`, `deepseek`, `anthropic`, `openai` (plus `searchEngineId` for `google-cse`). `chatgpt-subscription` and `google` use your senpi login when `apiKey` is omitted; see [Listing login-based routes](#listing-login-based-routes).
|
|
79
|
+
|
|
80
|
+
`keenable` ([Keenable](https://keenable.ai)) takes an `apiKey` but also works without one: a keyed entry posts to `https://api.keenable.ai/v1/search` with `X-API-Key`, while a keyless entry uses the shared public tier at `/v1/search/public` and identifies itself with an `X-Keenable-Title` app header instead of a credential. Because the public tier is a per-IP pool, `keenable` is never in the no-config default chain — list it explicitly in `websearch.json`. A single `allowedDomains` entry maps to Keenable's native `site` filter; multiple or blocked domains stay `site:`/`-site:` terms in the query.
|
|
79
81
|
|
|
80
82
|
## Limit or turn off the free engines
|
|
81
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.10-
|
|
3
|
+
"version": "2026.10.10-4",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@earendil-works/chord": "0.99.1",
|
|
60
|
-
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-
|
|
61
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-
|
|
62
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-
|
|
60
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-4",
|
|
61
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-4",
|
|
62
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-4",
|
|
63
63
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
64
64
|
"chalk": "6.0.0",
|
|
65
65
|
"cross-spawn": "7.0.6",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"yaml": "2.9.1",
|
|
79
79
|
"@anthropic-ai/claude-agent-sdk": "0.3.289",
|
|
80
80
|
"@anthropic-ai/sdk": "0.127.0",
|
|
81
|
-
"@code-yeongyu/senpi-codemode": "2026.10.10-
|
|
82
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-
|
|
81
|
+
"@code-yeongyu/senpi-codemode": "2026.10.10-4",
|
|
82
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-4",
|
|
83
83
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
84
84
|
"@mozilla/readability": "0.6.0",
|
|
85
85
|
"linkedom": "0.18.13",
|