@gajae-code/coding-agent 0.11.6 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +56 -6
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +57 -6
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/index.ts +4 -5
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +240 -149
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +319 -85
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as fsSync from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { MAX_UNIX_SOCKET_PATH_BYTES } from "../../harness-control-plane/storage";
|
|
6
|
+
import { authenticatePreface } from "./auth-preface";
|
|
7
|
+
import type { ServeHandle, ServeOptions } from "./index";
|
|
8
|
+
import type { RelayOptions, RelayPair, TransportError } from "./relay";
|
|
9
|
+
import { startRelayPair } from "./relay";
|
|
10
|
+
|
|
11
|
+
function socketFailure(code: string): Error {
|
|
12
|
+
return new Error(code);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function validateSocketPath(socketPath: string): Promise<void> {
|
|
16
|
+
if (process.platform === "win32") throw socketFailure("unsupported_platform");
|
|
17
|
+
if (Buffer.byteLength(socketPath) > MAX_UNIX_SOCKET_PATH_BYTES) throw socketFailure("socket_path_too_long");
|
|
18
|
+
try {
|
|
19
|
+
await fs.lstat(socketPath);
|
|
20
|
+
throw socketFailure("socket_path_in_use");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof Error && error.message === "socket_path_in_use") throw error;
|
|
23
|
+
const code = error as NodeJS.ErrnoException;
|
|
24
|
+
if (code.code !== "ENOENT") throw error;
|
|
25
|
+
}
|
|
26
|
+
let parent: Awaited<ReturnType<typeof fs.stat>>;
|
|
27
|
+
try {
|
|
28
|
+
parent = await fs.stat(path.dirname(socketPath));
|
|
29
|
+
} catch {
|
|
30
|
+
throw socketFailure("socket_dir_insecure");
|
|
31
|
+
}
|
|
32
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 0o022) !== 0)
|
|
33
|
+
throw socketFailure("socket_dir_insecure");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sendTransportError(socket: net.Socket, error: TransportError): void {
|
|
37
|
+
socket.write(`${JSON.stringify(error)}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Serves authenticated SDK JSONL clients over a private Unix-domain socket. */
|
|
41
|
+
export async function startSocketServe(
|
|
42
|
+
options: ServeOptions & { socketPath: string; webSocketFactory?: RelayOptions["webSocketFactory"] },
|
|
43
|
+
): Promise<ServeHandle> {
|
|
44
|
+
await validateSocketPath(options.socketPath);
|
|
45
|
+
const pairs = new Set<RelayPair>();
|
|
46
|
+
const sockets = new Set<net.Socket>();
|
|
47
|
+
const connectionTasks = new Set<Promise<void>>();
|
|
48
|
+
const dialControllers = new Set<AbortController>();
|
|
49
|
+
let server: net.Server | undefined;
|
|
50
|
+
let ownedIdentity: { dev: number; ino: number } | undefined;
|
|
51
|
+
let closing: Promise<void> | undefined;
|
|
52
|
+
const done = Promise.withResolvers<void>();
|
|
53
|
+
void done.promise.catch(() => undefined);
|
|
54
|
+
|
|
55
|
+
const removeOwnedSocket = async (): Promise<void> => {
|
|
56
|
+
if (!ownedIdentity) return;
|
|
57
|
+
try {
|
|
58
|
+
const current = await fs.lstat(options.socketPath);
|
|
59
|
+
if (current.isSocket() && current.dev === ownedIdentity.dev && current.ino === ownedIdentity.ino)
|
|
60
|
+
await fs.unlink(options.socketPath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const removeOwnedSocketSync = (): void => {
|
|
67
|
+
if (!ownedIdentity) return;
|
|
68
|
+
try {
|
|
69
|
+
const current = fsSync.lstatSync(options.socketPath);
|
|
70
|
+
if (current.isSocket() && current.dev === ownedIdentity.dev && current.ino === ownedIdentity.ino)
|
|
71
|
+
fsSync.unlinkSync(options.socketPath);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const cleanupOnExit = (): void => removeOwnedSocketSync();
|
|
77
|
+
process.once("exit", cleanupOnExit);
|
|
78
|
+
const close = async (fatalError?: Error): Promise<void> => {
|
|
79
|
+
if (closing) return await closing;
|
|
80
|
+
closing = (async () => {
|
|
81
|
+
const errors: Error[] = fatalError ? [fatalError] : [];
|
|
82
|
+
for (const controller of dialControllers) controller.abort();
|
|
83
|
+
for (const socket of sockets) socket.destroy();
|
|
84
|
+
const pairResults = await Promise.allSettled([...pairs].map(pair => pair.close()));
|
|
85
|
+
for (const result of pairResults) {
|
|
86
|
+
if (result.status === "rejected")
|
|
87
|
+
errors.push(result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
|
|
88
|
+
}
|
|
89
|
+
const connectionResults = await Promise.allSettled([...connectionTasks]);
|
|
90
|
+
for (const result of connectionResults) {
|
|
91
|
+
if (result.status === "rejected")
|
|
92
|
+
errors.push(result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
|
|
93
|
+
}
|
|
94
|
+
let displacedPath: string | undefined;
|
|
95
|
+
if (server?.listening && ownedIdentity) {
|
|
96
|
+
try {
|
|
97
|
+
const current = await fs.lstat(options.socketPath).catch(() => undefined);
|
|
98
|
+
if (current && (current.dev !== ownedIdentity.dev || current.ino !== ownedIdentity.ino)) {
|
|
99
|
+
displacedPath = `${options.socketPath}.${process.pid}.closing`;
|
|
100
|
+
await fs.rename(options.socketPath, displacedPath);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (server?.listening) {
|
|
107
|
+
try {
|
|
108
|
+
await new Promise<void>((resolve, reject) =>
|
|
109
|
+
server?.close(error => (error ? reject(error) : resolve())),
|
|
110
|
+
);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (displacedPath) {
|
|
116
|
+
try {
|
|
117
|
+
await fs.rename(displacedPath, options.socketPath);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await removeOwnedSocket();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
126
|
+
}
|
|
127
|
+
process.removeListener("exit", cleanupOnExit);
|
|
128
|
+
if (errors.length > 0) {
|
|
129
|
+
const error = new AggregateError(errors, "socket_serve_close_failed");
|
|
130
|
+
done.reject(error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
done.resolve();
|
|
134
|
+
})();
|
|
135
|
+
return await closing;
|
|
136
|
+
};
|
|
137
|
+
const handleConnection = async (socket: net.Socket, signal: AbortSignal): Promise<void> => {
|
|
138
|
+
sockets.add(socket);
|
|
139
|
+
socket.once("close", () => sockets.delete(socket));
|
|
140
|
+
try {
|
|
141
|
+
const initialDownstreamBytes = await authenticatePreface(socket, options.token);
|
|
142
|
+
if (closing) {
|
|
143
|
+
socket.destroy();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const pair = await startRelayPair({
|
|
147
|
+
...options,
|
|
148
|
+
signal,
|
|
149
|
+
downstream: socket,
|
|
150
|
+
downstreamSink: socket,
|
|
151
|
+
initialDownstreamBytes,
|
|
152
|
+
onTransportError: error => sendTransportError(socket, error),
|
|
153
|
+
});
|
|
154
|
+
pairs.add(pair);
|
|
155
|
+
try {
|
|
156
|
+
await pair.done.catch(() => undefined);
|
|
157
|
+
} finally {
|
|
158
|
+
pairs.delete(pair);
|
|
159
|
+
socket.end();
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
socket.destroy();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
server = net.createServer(socket => {
|
|
166
|
+
if (closing) {
|
|
167
|
+
socket.destroy();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
dialControllers.add(controller);
|
|
172
|
+
const task = handleConnection(socket, controller.signal).finally(() => {
|
|
173
|
+
dialControllers.delete(controller);
|
|
174
|
+
connectionTasks.delete(task);
|
|
175
|
+
});
|
|
176
|
+
connectionTasks.add(task);
|
|
177
|
+
void task;
|
|
178
|
+
});
|
|
179
|
+
server.on("error", error => void close(error).catch(() => undefined));
|
|
180
|
+
await new Promise<void>((resolve, reject) => {
|
|
181
|
+
server?.once("error", reject);
|
|
182
|
+
server?.listen(options.socketPath, () => resolve());
|
|
183
|
+
});
|
|
184
|
+
const bound = await fs.lstat(options.socketPath);
|
|
185
|
+
if (!bound.isSocket()) {
|
|
186
|
+
await close();
|
|
187
|
+
throw socketFailure("socket_bind_failed");
|
|
188
|
+
}
|
|
189
|
+
ownedIdentity = { dev: bound.dev, ino: bound.ino };
|
|
190
|
+
try {
|
|
191
|
+
await fs.chmod(options.socketPath, 0o600);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
await close();
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return { close, done: done.promise };
|
|
197
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServeHandle, ServeOptions } from "./index";
|
|
2
|
+
import { startRelayPair } from "./relay";
|
|
3
|
+
|
|
4
|
+
function writeDiagnostic(value: unknown): void {
|
|
5
|
+
process.stderr.write(`${JSON.stringify(value)}\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Serves one parent-owned JSONL connection over the process standard streams. */
|
|
9
|
+
export async function startStdioServe(options: ServeOptions): Promise<ServeHandle> {
|
|
10
|
+
const pair = await startRelayPair({
|
|
11
|
+
...options,
|
|
12
|
+
downstream: process.stdin,
|
|
13
|
+
downstreamSink: process.stdout,
|
|
14
|
+
onTransportError: writeDiagnostic,
|
|
15
|
+
});
|
|
16
|
+
return { close: pair.close, done: pair.done };
|
|
17
|
+
}
|
|
@@ -105,6 +105,16 @@ import {
|
|
|
105
105
|
type FallbackAttemptToken,
|
|
106
106
|
type FallbackTriggerClass,
|
|
107
107
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
108
|
+
import {
|
|
109
|
+
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
110
|
+
BTW_MAX_QUESTION_UTF8_BYTES,
|
|
111
|
+
BTW_STREAM_IDLE_TIMEOUT_MS,
|
|
112
|
+
BTW_STREAM_TOTAL_TIMEOUT_MS,
|
|
113
|
+
type BtwTextExchange,
|
|
114
|
+
boundBtwExchanges,
|
|
115
|
+
truncateUtf8,
|
|
116
|
+
utf8ByteLength,
|
|
117
|
+
} from "./btw-contract";
|
|
108
118
|
|
|
109
119
|
export interface ForkContextSeedMetadata {
|
|
110
120
|
sourceSessionId: string;
|
|
@@ -827,7 +837,6 @@ function isLocalModelEndpoint(model: Model | undefined): boolean {
|
|
|
827
837
|
}
|
|
828
838
|
|
|
829
839
|
const IRC_REPLY_MAX_BYTES = 4096;
|
|
830
|
-
const BTW_ESTABLISHMENT_TIMEOUT_MS = 15_000;
|
|
831
840
|
|
|
832
841
|
export type EphemeralTurnPurpose = "btw" | "background";
|
|
833
842
|
|
|
@@ -837,8 +846,34 @@ interface EphemeralTurnBaseArgs {
|
|
|
837
846
|
signal?: AbortSignal;
|
|
838
847
|
}
|
|
839
848
|
|
|
849
|
+
export interface BtwRoleTextMessage {
|
|
850
|
+
role: "user" | "assistant";
|
|
851
|
+
text: string;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export interface BtwConversationScope {
|
|
855
|
+
model: Model;
|
|
856
|
+
systemPrompt: string[];
|
|
857
|
+
messages: BtwRoleTextMessage[];
|
|
858
|
+
thinkingLevel: ThinkingLevel;
|
|
859
|
+
hideThinkingSummary: boolean;
|
|
860
|
+
serviceTier: ServiceTier | undefined;
|
|
861
|
+
credentialSessionId: string;
|
|
862
|
+
providerAffinitySessionId: string;
|
|
863
|
+
sideSessionId: string;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export interface BtwTurnCapture {
|
|
867
|
+
question: string;
|
|
868
|
+
scope: BtwConversationScope | undefined;
|
|
869
|
+
}
|
|
870
|
+
|
|
840
871
|
export type EphemeralTurnArgs =
|
|
841
|
-
| (EphemeralTurnBaseArgs & {
|
|
872
|
+
| (Omit<EphemeralTurnBaseArgs, "promptText"> & {
|
|
873
|
+
purpose: "btw";
|
|
874
|
+
turn: BtwTurnCapture;
|
|
875
|
+
contextExchanges?: readonly BtwTextExchange[];
|
|
876
|
+
})
|
|
842
877
|
| (EphemeralTurnBaseArgs & {
|
|
843
878
|
purpose?: "background";
|
|
844
879
|
/** Internal caller-supplied, non-persistent context such as the IRC roster. */
|
|
@@ -13993,167 +14028,127 @@ export class AgentSession {
|
|
|
13993
14028
|
this.#ircRosterClaim = null;
|
|
13994
14029
|
}
|
|
13995
14030
|
|
|
14031
|
+
createBtwConversationScope(instruction: string): BtwConversationScope {
|
|
14032
|
+
const model = this.model;
|
|
14033
|
+
if (!model) throw new Error("No active model on session");
|
|
14034
|
+
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
14035
|
+
return {
|
|
14036
|
+
model,
|
|
14037
|
+
systemPrompt: [...this.systemPrompt, instruction],
|
|
14038
|
+
messages: this.#projectBtwVisibleText(this.buildDisplaySessionContext().messages),
|
|
14039
|
+
thinkingLevel: this.thinkingLevel ?? ThinkingLevel.Off,
|
|
14040
|
+
hideThinkingSummary: this.agent.hideThinkingSummary ?? false,
|
|
14041
|
+
serviceTier: this.serviceTier,
|
|
14042
|
+
credentialSessionId: providerAffinitySessionId,
|
|
14043
|
+
providerAffinitySessionId,
|
|
14044
|
+
sideSessionId: `${providerAffinitySessionId}:btw:${crypto.randomUUID()}`,
|
|
14045
|
+
};
|
|
14046
|
+
}
|
|
14047
|
+
|
|
14048
|
+
#projectBtwVisibleText(messages: readonly AgentMessage[]): BtwRoleTextMessage[] {
|
|
14049
|
+
const projected: BtwRoleTextMessage[] = [];
|
|
14050
|
+
for (const message of messages) {
|
|
14051
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
14052
|
+
const text = (
|
|
14053
|
+
typeof message.content === "string"
|
|
14054
|
+
? message.content
|
|
14055
|
+
: message.content
|
|
14056
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
14057
|
+
.map(block => block.text)
|
|
14058
|
+
.join("")
|
|
14059
|
+
).trim();
|
|
14060
|
+
if (!text) continue;
|
|
14061
|
+
projected.push({ role: message.role, text });
|
|
14062
|
+
}
|
|
14063
|
+
return projected;
|
|
14064
|
+
}
|
|
14065
|
+
|
|
14066
|
+
#buildBtwAssistantMessage(text: string): AssistantMessage {
|
|
14067
|
+
return {
|
|
14068
|
+
role: "assistant",
|
|
14069
|
+
content: [{ type: "text", text }],
|
|
14070
|
+
api: "btw",
|
|
14071
|
+
provider: "btw",
|
|
14072
|
+
model: "btw",
|
|
14073
|
+
usage: {
|
|
14074
|
+
input: 0,
|
|
14075
|
+
output: 0,
|
|
14076
|
+
cacheRead: 0,
|
|
14077
|
+
cacheWrite: 0,
|
|
14078
|
+
totalTokens: 0,
|
|
14079
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14080
|
+
},
|
|
14081
|
+
stopReason: "stop",
|
|
14082
|
+
timestamp: 0,
|
|
14083
|
+
};
|
|
14084
|
+
}
|
|
13996
14085
|
/**
|
|
13997
|
-
* Run a single ephemeral side-channel turn
|
|
13998
|
-
*
|
|
13999
|
-
*
|
|
14000
|
-
*
|
|
14086
|
+
* Run a single ephemeral side-channel turn without modifying session history.
|
|
14087
|
+
* Background turns retain IRC/session behavior. `/btw` turns require a
|
|
14088
|
+
* pre-frozen, visible-text-only scope and bypass extension context transforms,
|
|
14089
|
+
* provider observability hooks, and session persistence surfaces.
|
|
14001
14090
|
*/
|
|
14002
14091
|
async runEphemeralTurn(args: EphemeralTurnArgs): Promise<EphemeralTurnResult> {
|
|
14003
14092
|
args.signal?.throwIfAborted();
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
const
|
|
14008
|
-
? this.#buildBtwEphemeralSnapshot(
|
|
14009
|
-
cloneJsonValueForForkSeed(this.buildDisplaySessionContext().messages),
|
|
14010
|
-
args.promptText,
|
|
14011
|
-
)
|
|
14012
|
-
: undefined;
|
|
14013
|
-
const model = this.model;
|
|
14014
|
-
const systemPrompt = isBtw ? [...this.systemPrompt] : undefined;
|
|
14015
|
-
const thinkingLevel = this.thinkingLevel;
|
|
14016
|
-
const hideThinkingSummary = this.agent.hideThinkingSummary;
|
|
14017
|
-
const serviceTier = this.serviceTier;
|
|
14018
|
-
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
14019
|
-
const sideSessionId = isBtw ? `${providerAffinitySessionId}:btw:${crypto.randomUUID()}` : undefined;
|
|
14020
|
-
const backgroundArgs = isBtw ? undefined : args;
|
|
14021
|
-
const callerOwnsRosterClaim = backgroundArgs?.ircRosterClaim !== undefined;
|
|
14022
|
-
const rosterClaim = isBtw
|
|
14023
|
-
? null
|
|
14024
|
-
: callerOwnsRosterClaim
|
|
14025
|
-
? backgroundArgs?.ircRosterClaim
|
|
14026
|
-
: this.#claimIrcRosterCandidate();
|
|
14093
|
+
if (args.purpose === "btw") return await this.#runBtwTurn(args);
|
|
14094
|
+
|
|
14095
|
+
const callerOwnsRosterClaim = args.ircRosterClaim !== undefined;
|
|
14096
|
+
const rosterClaim = callerOwnsRosterClaim ? args.ircRosterClaim : this.#claimIrcRosterCandidate();
|
|
14027
14097
|
const rosterClaimIsCurrent = () =>
|
|
14028
14098
|
!rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
14029
|
-
const prependMessagesValid = () => rosterClaimIsCurrent() &&
|
|
14099
|
+
const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
|
|
14030
14100
|
const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
|
|
14031
14101
|
try {
|
|
14102
|
+
const model = this.model;
|
|
14032
14103
|
if (!model) throw new Error("No active model on session");
|
|
14033
|
-
const
|
|
14034
|
-
const apiKey = await awaitEphemeralAbort(
|
|
14035
|
-
this.#modelRegistry.getApiKey(model, credentialSessionId),
|
|
14036
|
-
args.signal,
|
|
14037
|
-
);
|
|
14104
|
+
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
|
|
14038
14105
|
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
14039
|
-
|
|
14040
14106
|
const prependMessages = prependMessagesValid()
|
|
14041
|
-
? [...(rosterMessage ? [rosterMessage] : []), ...(
|
|
14107
|
+
? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
|
|
14042
14108
|
: undefined;
|
|
14043
|
-
let snapshot =
|
|
14109
|
+
let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
|
|
14044
14110
|
let llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
|
|
14045
|
-
if (
|
|
14111
|
+
if (prependMessages && !prependMessagesValid()) {
|
|
14046
14112
|
snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
14047
14113
|
llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
|
|
14048
14114
|
}
|
|
14049
|
-
const context: Context = {
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
|
|
14115
|
+
const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
|
|
14116
|
+
const ephemeralSessionId = crypto.randomUUID();
|
|
14117
|
+
const options = this.prepareSimpleStreamOptions(
|
|
14118
|
+
{
|
|
14119
|
+
apiKey,
|
|
14120
|
+
sessionId: ephemeralSessionId,
|
|
14121
|
+
metadata: buildSessionMetadata(
|
|
14122
|
+
ephemeralSessionId,
|
|
14123
|
+
model.provider,
|
|
14124
|
+
this.#modelRegistry.authStorage,
|
|
14125
|
+
this.sessionId,
|
|
14126
|
+
),
|
|
14127
|
+
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
14128
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
14129
|
+
serviceTier: this.serviceTier,
|
|
14130
|
+
signal: args.signal,
|
|
14131
|
+
toolChoice: "none",
|
|
14132
|
+
},
|
|
14133
|
+
model.provider,
|
|
14134
|
+
);
|
|
14135
|
+
args.signal?.throwIfAborted();
|
|
14055
14136
|
let replyText = "";
|
|
14056
14137
|
let assistantMessage: AssistantMessage | undefined;
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
)
|
|
14065
|
-
|
|
14066
|
-
let establishmentTimer: NodeJS.Timeout | undefined;
|
|
14067
|
-
try {
|
|
14068
|
-
requestSignal.throwIfAborted();
|
|
14069
|
-
const options: SimpleStreamOptions = {
|
|
14070
|
-
apiKey,
|
|
14071
|
-
sessionId: sideSessionId!,
|
|
14072
|
-
metadata: buildSessionMetadata(
|
|
14073
|
-
sideSessionId!,
|
|
14074
|
-
model.provider,
|
|
14075
|
-
this.#modelRegistry.authStorage,
|
|
14076
|
-
providerAffinitySessionId,
|
|
14077
|
-
),
|
|
14078
|
-
reasoning: toReasoningEffort(thinkingLevel),
|
|
14079
|
-
hideThinkingSummary,
|
|
14080
|
-
serviceTier,
|
|
14081
|
-
onPayload: this.#onPayload,
|
|
14082
|
-
signal: requestSignal,
|
|
14083
|
-
toolChoice: "none",
|
|
14084
|
-
requestMaxRetries: 0,
|
|
14085
|
-
streamMaxRetries: 0,
|
|
14086
|
-
streamFirstEventTimeoutMs: 0,
|
|
14087
|
-
};
|
|
14088
|
-
establishmentTimer = setTimeout(
|
|
14089
|
-
() => establishmentAbort.abort(timeoutError),
|
|
14090
|
-
BTW_ESTABLISHMENT_TIMEOUT_MS,
|
|
14091
|
-
);
|
|
14092
|
-
establishmentTimer.unref?.();
|
|
14093
|
-
const stream = streamSimple(model, context, options);
|
|
14094
|
-
let receivedProviderEvent = false;
|
|
14095
|
-
for await (const event of stream) {
|
|
14096
|
-
if (!receivedProviderEvent && event.type !== "start") {
|
|
14097
|
-
receivedProviderEvent = true;
|
|
14098
|
-
clearTimeout(establishmentTimer);
|
|
14099
|
-
establishmentTimer = undefined;
|
|
14100
|
-
}
|
|
14101
|
-
if (event.type === "text_delta") {
|
|
14102
|
-
replyText += event.delta;
|
|
14103
|
-
args.onTextDelta?.(event.delta);
|
|
14104
|
-
} else if (event.type === "done") {
|
|
14105
|
-
assistantMessage = event.message;
|
|
14106
|
-
break;
|
|
14107
|
-
} else if (event.type === "error") {
|
|
14108
|
-
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
14109
|
-
}
|
|
14110
|
-
}
|
|
14111
|
-
requestSignal.throwIfAborted();
|
|
14112
|
-
} catch (error) {
|
|
14113
|
-
if (establishmentAbort.signal.aborted && !args.signal?.aborted) throw timeoutError;
|
|
14114
|
-
throw error;
|
|
14115
|
-
} finally {
|
|
14116
|
-
if (establishmentTimer) clearTimeout(establishmentTimer);
|
|
14117
|
-
}
|
|
14118
|
-
} else {
|
|
14119
|
-
const ephemeralSessionId = crypto.randomUUID();
|
|
14120
|
-
const options = this.prepareSimpleStreamOptions(
|
|
14121
|
-
{
|
|
14122
|
-
apiKey,
|
|
14123
|
-
sessionId: ephemeralSessionId,
|
|
14124
|
-
metadata: buildSessionMetadata(
|
|
14125
|
-
ephemeralSessionId,
|
|
14126
|
-
model.provider,
|
|
14127
|
-
this.#modelRegistry.authStorage,
|
|
14128
|
-
this.sessionId,
|
|
14129
|
-
),
|
|
14130
|
-
reasoning: toReasoningEffort(thinkingLevel),
|
|
14131
|
-
hideThinkingSummary,
|
|
14132
|
-
serviceTier,
|
|
14133
|
-
signal: args.signal,
|
|
14134
|
-
toolChoice: "none",
|
|
14135
|
-
},
|
|
14136
|
-
model.provider,
|
|
14137
|
-
);
|
|
14138
|
-
args.signal?.throwIfAborted();
|
|
14139
|
-
const stream = streamSimple(model, context, options);
|
|
14140
|
-
for await (const event of stream) {
|
|
14141
|
-
if (event.type === "text_delta") {
|
|
14142
|
-
replyText += event.delta;
|
|
14143
|
-
args.onTextDelta?.(event.delta);
|
|
14144
|
-
} else if (event.type === "done") {
|
|
14145
|
-
assistantMessage = event.message;
|
|
14146
|
-
break;
|
|
14147
|
-
} else if (event.type === "error") {
|
|
14148
|
-
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
14149
|
-
}
|
|
14138
|
+
for await (const event of streamSimple(model, context, options)) {
|
|
14139
|
+
if (event.type === "text_delta") {
|
|
14140
|
+
replyText += event.delta;
|
|
14141
|
+
args.onTextDelta?.(event.delta);
|
|
14142
|
+
} else if (event.type === "done") {
|
|
14143
|
+
assistantMessage = event.message;
|
|
14144
|
+
break;
|
|
14145
|
+
} else if (event.type === "error") {
|
|
14146
|
+
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
14150
14147
|
}
|
|
14151
14148
|
}
|
|
14152
|
-
|
|
14153
14149
|
if (!assistantMessage) throw new Error("Ephemeral turn ended without a final message");
|
|
14154
14150
|
args.signal?.throwIfAborted();
|
|
14155
14151
|
if (
|
|
14156
|
-
!isBtw &&
|
|
14157
14152
|
!callerOwnsRosterClaim &&
|
|
14158
14153
|
rosterClaim &&
|
|
14159
14154
|
assistantMessage.stopReason !== "error" &&
|
|
@@ -14161,21 +14156,117 @@ export class AgentSession {
|
|
|
14161
14156
|
) {
|
|
14162
14157
|
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
14163
14158
|
}
|
|
14164
|
-
args.signal?.throwIfAborted();
|
|
14165
14159
|
return { replyText: replyText.trim(), assistantMessage };
|
|
14166
14160
|
} finally {
|
|
14167
|
-
if (!
|
|
14168
|
-
|
|
14161
|
+
if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
14162
|
+
}
|
|
14163
|
+
}
|
|
14164
|
+
|
|
14165
|
+
async #runBtwTurn(args: Extract<EphemeralTurnArgs, { purpose: "btw" }>): Promise<EphemeralTurnResult> {
|
|
14166
|
+
const model = args.turn.scope?.model;
|
|
14167
|
+
const credentialSessionId = args.turn.scope?.credentialSessionId;
|
|
14168
|
+
if (!model || !credentialSessionId) throw new Error("The /btw conversation scope was scrubbed.");
|
|
14169
|
+
if (utf8ByteLength(args.turn.question) > BTW_MAX_QUESTION_UTF8_BYTES) {
|
|
14170
|
+
throw new RangeError(`/btw questions are limited to ${BTW_MAX_QUESTION_UTF8_BYTES} UTF-8 bytes.`);
|
|
14171
|
+
}
|
|
14172
|
+
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
|
|
14173
|
+
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
14174
|
+
const scope = args.turn.scope;
|
|
14175
|
+
if (!scope) throw new Error("The /btw conversation scope was scrubbed.");
|
|
14176
|
+
const messages = scope.messages.map(message => ({
|
|
14177
|
+
role: message.role,
|
|
14178
|
+
content: [{ type: "text" as const, text: message.text }],
|
|
14179
|
+
})) as Message[];
|
|
14180
|
+
for (const exchange of boundBtwExchanges(args.contextExchanges ?? [])) {
|
|
14181
|
+
messages.push({
|
|
14182
|
+
role: "user",
|
|
14183
|
+
content: [{ type: "text", text: exchange.question }],
|
|
14184
|
+
} as Message);
|
|
14185
|
+
messages.push({
|
|
14186
|
+
role: "assistant",
|
|
14187
|
+
content: [{ type: "text", text: exchange.answer }],
|
|
14188
|
+
} as Message);
|
|
14189
|
+
}
|
|
14190
|
+
messages.push({
|
|
14191
|
+
role: "user",
|
|
14192
|
+
content: [{ type: "text", text: args.turn.question }],
|
|
14193
|
+
} as Message);
|
|
14194
|
+
const context: Context = { systemPrompt: scope.systemPrompt, messages, tools: [] };
|
|
14195
|
+
const timeoutAbort = new AbortController();
|
|
14196
|
+
const requestSignal = args.signal ? AbortSignal.any([args.signal, timeoutAbort.signal]) : timeoutAbort.signal;
|
|
14197
|
+
const options: SimpleStreamOptions = {
|
|
14198
|
+
apiKey,
|
|
14199
|
+
sessionId: scope.sideSessionId,
|
|
14200
|
+
reasoning: toReasoningEffort(scope.thinkingLevel),
|
|
14201
|
+
hideThinkingSummary: scope.hideThinkingSummary,
|
|
14202
|
+
serviceTier: scope.serviceTier,
|
|
14203
|
+
signal: requestSignal,
|
|
14204
|
+
toolChoice: "none",
|
|
14205
|
+
requestMaxRetries: 0,
|
|
14206
|
+
streamMaxRetries: 0,
|
|
14207
|
+
streamFirstEventTimeoutMs: 0,
|
|
14208
|
+
};
|
|
14209
|
+
const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
|
|
14210
|
+
let replyText = "";
|
|
14211
|
+
let completed = false;
|
|
14212
|
+
let active = true;
|
|
14213
|
+
let onTextDelta = args.onTextDelta;
|
|
14214
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
14215
|
+
const timeout = (message: string) => {
|
|
14216
|
+
const error = new Error(message);
|
|
14217
|
+
error.name = "TimeoutError";
|
|
14218
|
+
timeoutAbort.abort(error);
|
|
14219
|
+
};
|
|
14220
|
+
const resetIdleTimer = () => {
|
|
14221
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
14222
|
+
idleTimer = setTimeout(
|
|
14223
|
+
() => timeout(`/btw provider was idle for ${BTW_STREAM_IDLE_TIMEOUT_MS / 1000} seconds`),
|
|
14224
|
+
BTW_STREAM_IDLE_TIMEOUT_MS,
|
|
14225
|
+
);
|
|
14226
|
+
idleTimer.unref?.();
|
|
14227
|
+
};
|
|
14228
|
+
const totalTimer = setTimeout(
|
|
14229
|
+
() => timeout(`/btw provider exceeded ${BTW_STREAM_TOTAL_TIMEOUT_MS / 1000} seconds`),
|
|
14230
|
+
BTW_STREAM_TOTAL_TIMEOUT_MS,
|
|
14231
|
+
);
|
|
14232
|
+
totalTimer.unref?.();
|
|
14233
|
+
resetIdleTimer();
|
|
14234
|
+
const consume = async () => {
|
|
14235
|
+
while (active) {
|
|
14236
|
+
const result = await iterator.next();
|
|
14237
|
+
if (result.done || !active) break;
|
|
14238
|
+
resetIdleTimer();
|
|
14239
|
+
const event = result.value;
|
|
14240
|
+
if (event.type === "text_delta") {
|
|
14241
|
+
const bounded = truncateUtf8(replyText + event.delta, BTW_MAX_ANSWER_UTF8_BYTES);
|
|
14242
|
+
const delta = bounded.slice(replyText.length);
|
|
14243
|
+
replyText = bounded;
|
|
14244
|
+
if (delta) onTextDelta?.(delta);
|
|
14245
|
+
} else if (event.type === "done") {
|
|
14246
|
+
completed = true;
|
|
14247
|
+
break;
|
|
14248
|
+
} else if (event.type === "error") {
|
|
14249
|
+
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
14250
|
+
}
|
|
14169
14251
|
}
|
|
14252
|
+
};
|
|
14253
|
+
try {
|
|
14254
|
+
await awaitEphemeralAbort(consume(), requestSignal);
|
|
14255
|
+
requestSignal.throwIfAborted();
|
|
14256
|
+
if (!completed) throw new Error("Ephemeral turn ended without a final message");
|
|
14257
|
+
const finalText = replyText.trim();
|
|
14258
|
+
return { replyText: finalText, assistantMessage: this.#buildBtwAssistantMessage(finalText) };
|
|
14259
|
+
} finally {
|
|
14260
|
+
active = false;
|
|
14261
|
+
onTextDelta = undefined;
|
|
14262
|
+
replyText = "";
|
|
14263
|
+
context.messages = [];
|
|
14264
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
14265
|
+
clearTimeout(totalTimer);
|
|
14266
|
+
void iterator.return?.().catch(() => undefined);
|
|
14170
14267
|
}
|
|
14171
14268
|
}
|
|
14172
14269
|
|
|
14173
|
-
/** Build a `/btw` snapshot from an already-cloned committed context. */
|
|
14174
|
-
#buildBtwEphemeralSnapshot(messages: AgentMessage[], promptText: string): AgentMessage[] {
|
|
14175
|
-
messages.push(this.#buildEphemeralPromptMessage(promptText));
|
|
14176
|
-
return messages;
|
|
14177
|
-
}
|
|
14178
|
-
|
|
14179
14270
|
/** Build a background snapshot with in-flight assistant and optional context. */
|
|
14180
14271
|
#buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
|
|
14181
14272
|
const messages = [...this.messages];
|