wotann 0.5.86 → 0.5.87
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.
|
@@ -36,8 +36,48 @@ export interface ToolResultEvent {
|
|
|
36
36
|
readonly toolName: string;
|
|
37
37
|
readonly result: string;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Emitted at an iteration boundary after the loop drained one or more
|
|
41
|
+
* user-side redirects (Hermes Gap 2 — interrupt-and-redirect). UI
|
|
42
|
+
* consumers can render a system bubble showing the redirected text so
|
|
43
|
+
* the user has visual confirmation the directive landed. The redirect
|
|
44
|
+
* is ALREADY appended to context by the loop before this event fires;
|
|
45
|
+
* receivers MUST NOT re-append it themselves.
|
|
46
|
+
*/
|
|
47
|
+
export interface RedirectReceivedEvent {
|
|
48
|
+
readonly kind: "redirect_received";
|
|
49
|
+
readonly count: number;
|
|
50
|
+
readonly messages: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export type RunAgentControlEvent = IterationStartEvent | ToolResultEvent | RedirectReceivedEvent;
|
|
40
53
|
export type AgentEvent = StreamChunk | RunAgentControlEvent;
|
|
54
|
+
/**
|
|
55
|
+
* Channel for user-side redirects (Hermes Gap 2). The TUI / RPC client
|
|
56
|
+
* pushes messages when the user sends a new directive WHILE the agent
|
|
57
|
+
* is streaming. The runAgent loop drains at each iteration boundary
|
|
58
|
+
* and appends them to context as new user turns, so the model absorbs
|
|
59
|
+
* the directive on the next turn — no context loss, no abort needed.
|
|
60
|
+
*
|
|
61
|
+
* Implementations MUST be safe to call concurrently (push from the
|
|
62
|
+
* UI thread, drain from the loop's async generator).
|
|
63
|
+
*/
|
|
64
|
+
export interface RedirectChannel {
|
|
65
|
+
/** Drain all pending redirects. Returns empty array when none queued. */
|
|
66
|
+
drain(): readonly string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Default in-memory channel. Pushes are appended to a queue; drain
|
|
70
|
+
* empties the queue atomically. The queue ignores empty-string pushes
|
|
71
|
+
* so an accidental Ctrl+R on an empty composer doesn't inject a blank
|
|
72
|
+
* user turn into the model context.
|
|
73
|
+
*/
|
|
74
|
+
export declare class InMemoryRedirectChannel implements RedirectChannel {
|
|
75
|
+
private queue;
|
|
76
|
+
push(message: string): void;
|
|
77
|
+
drain(): readonly string[];
|
|
78
|
+
/** Snapshot of pending count — for status-bar indicators. Read-only. */
|
|
79
|
+
get pending(): number;
|
|
80
|
+
}
|
|
41
81
|
export interface RunAgentOptions {
|
|
42
82
|
readonly prompt: string;
|
|
43
83
|
readonly images?: readonly string[];
|
|
@@ -49,6 +89,14 @@ export interface RunAgentOptions {
|
|
|
49
89
|
readonly signal?: AbortSignal;
|
|
50
90
|
readonly guardrails?: readonly GuardrailCheck[];
|
|
51
91
|
readonly guardrailContext?: GuardrailContext;
|
|
92
|
+
/**
|
|
93
|
+
* Optional user-redirect channel. When provided, the loop drains
|
|
94
|
+
* pending messages BEFORE each iteration's query and appends them
|
|
95
|
+
* to context as new user turns. Lets the user redirect a streaming
|
|
96
|
+
* agent without losing context (Hermes Gap 2). See
|
|
97
|
+
* {@link InMemoryRedirectChannel} for the default implementation.
|
|
98
|
+
*/
|
|
99
|
+
readonly redirectChannel?: RedirectChannel;
|
|
52
100
|
readonly query: (o: WotannQueryOptions) => AsyncGenerator<StreamChunk>;
|
|
53
101
|
readonly executeTool: (name: string, input: Record<string, unknown>) => Promise<string>;
|
|
54
102
|
}
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { evaluateGuardrails, } from "../guardrails/tripwire.js";
|
|
2
|
-
import { IntelligenceAmplifier, updateDoomLoopState } from "../intelligence/amplifier.js";
|
|
2
|
+
import { IntelligenceAmplifier, updateDoomLoopState, } from "../intelligence/amplifier.js";
|
|
3
|
+
/**
|
|
4
|
+
* Default in-memory channel. Pushes are appended to a queue; drain
|
|
5
|
+
* empties the queue atomically. The queue ignores empty-string pushes
|
|
6
|
+
* so an accidental Ctrl+R on an empty composer doesn't inject a blank
|
|
7
|
+
* user turn into the model context.
|
|
8
|
+
*/
|
|
9
|
+
export class InMemoryRedirectChannel {
|
|
10
|
+
queue = [];
|
|
11
|
+
push(message) {
|
|
12
|
+
const trimmed = message.trim();
|
|
13
|
+
if (trimmed.length === 0)
|
|
14
|
+
return;
|
|
15
|
+
this.queue.push(trimmed);
|
|
16
|
+
}
|
|
17
|
+
drain() {
|
|
18
|
+
if (this.queue.length === 0)
|
|
19
|
+
return [];
|
|
20
|
+
const out = this.queue;
|
|
21
|
+
this.queue = [];
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Snapshot of pending count — for status-bar indicators. Read-only. */
|
|
25
|
+
get pending() {
|
|
26
|
+
return this.queue.length;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
3
29
|
export async function* runAgent(opts) {
|
|
4
30
|
const maxIterations = opts.maxIterations ?? 8;
|
|
5
31
|
const guardrails = opts.guardrails ?? [];
|
|
@@ -20,6 +46,22 @@ export async function* runAgent(opts) {
|
|
|
20
46
|
while (iteration < maxIterations) {
|
|
21
47
|
iteration++;
|
|
22
48
|
yield { kind: "iteration_start", iteration };
|
|
49
|
+
// Hermes Gap 2 — drain user-side redirects BEFORE this iteration's
|
|
50
|
+
// query. Each pending message becomes a new user turn appended to
|
|
51
|
+
// context, so the model sees the directive on its next turn. No
|
|
52
|
+
// abort, no context loss. We emit a `redirect_received` control
|
|
53
|
+
// event so UI consumers can render a system breadcrumb.
|
|
54
|
+
if (opts.redirectChannel !== undefined) {
|
|
55
|
+
const pending = opts.redirectChannel.drain();
|
|
56
|
+
if (pending.length > 0) {
|
|
57
|
+
const next = [...context];
|
|
58
|
+
for (const msg of pending) {
|
|
59
|
+
next.push({ role: "user", content: msg });
|
|
60
|
+
}
|
|
61
|
+
context = next;
|
|
62
|
+
yield { kind: "redirect_received", count: pending.length, messages: pending };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
23
65
|
let fullContent = "";
|
|
24
66
|
let model;
|
|
25
67
|
let provider;
|
|
@@ -41,7 +41,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "#wotann-jsx/j
|
|
|
41
41
|
*/
|
|
42
42
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
43
43
|
import { Box, Text, useInput } from "ink";
|
|
44
|
-
import { runAgent } from "../../../core/runtime-agent-loop.js";
|
|
44
|
+
import { InMemoryRedirectChannel, runAgent } from "../../../core/runtime-agent-loop.js";
|
|
45
45
|
import { buildAgentToolContext } from "../../../core/agent-tool-context.js";
|
|
46
46
|
import { AGENT_TOOL_DEFINITIONS, executeAgentTool } from "../../../tools/agent-tools.js";
|
|
47
47
|
import { ThemeProvider, useThemeTone } from "../../theme/context.js";
|
|
@@ -217,6 +217,13 @@ function AppV3Inner({ providers, initialModel, initialProvider, messages, isStre
|
|
|
217
217
|
const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
|
|
218
218
|
const activeRunRef = useRef(null);
|
|
219
219
|
const sideActiveRunRef = useRef(null);
|
|
220
|
+
// Hermes Gap 2 — interrupt-and-redirect: persistent channel for the
|
|
221
|
+
// host composer's mid-stream redirects. Ctrl+R captures the current
|
|
222
|
+
// draft and pushes it here; runAgent's iteration loop drains pending
|
|
223
|
+
// redirects at the next iteration boundary and appends them to
|
|
224
|
+
// context as new user turns. One channel per AppV3Inner instance so
|
|
225
|
+
// pending redirects survive within a session.
|
|
226
|
+
const redirectChannelRef = useRef(new InMemoryRedirectChannel());
|
|
220
227
|
const [sidePaneDraft, setSidePaneDraft] = useState("");
|
|
221
228
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
222
229
|
useEffect(() => {
|
|
@@ -241,6 +248,16 @@ function AppV3Inner({ providers, initialModel, initialProvider, messages, isStre
|
|
|
241
248
|
// composer is empty so we don't intercept the dot from a typed
|
|
242
249
|
// slash-command argument or filename.
|
|
243
250
|
useInput((input, key) => {
|
|
251
|
+
// Hermes Gap 2 — Ctrl+R: queue the composer draft as a mid-stream
|
|
252
|
+
// redirect. ONLY fires when the agent is currently streaming AND
|
|
253
|
+
// the draft has non-whitespace content. The channel's drain runs
|
|
254
|
+
// at the next iteration boundary, so the user sees a system
|
|
255
|
+
// "redirect queued" bubble within ~1 turn.
|
|
256
|
+
if (key.ctrl && input === "r" && isStreaming && draftValue.trim().length > 0) {
|
|
257
|
+
redirectChannelRef.current.push(draftValue);
|
|
258
|
+
setDraftValue("");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
244
261
|
if (key.ctrl && (input === "k" || input === "p")) {
|
|
245
262
|
overlay.open("palette");
|
|
246
263
|
return;
|
|
@@ -342,6 +359,7 @@ function AppV3Inner({ providers, initialModel, initialProvider, messages, isStre
|
|
|
342
359
|
provider: initialProvider || undefined,
|
|
343
360
|
tools: AGENT_TOOL_DEFINITIONS,
|
|
344
361
|
signal: abortController.signal,
|
|
362
|
+
redirectChannel: redirectChannelRef.current,
|
|
345
363
|
query: (o) => runtime.query(o),
|
|
346
364
|
executeTool: (name, input) => executeAgentTool(name, input, buildAgentToolContext(runtime, {
|
|
347
365
|
workingDir: runtime.getWorkingDir(),
|
|
@@ -362,6 +380,20 @@ function AppV3Inner({ providers, initialModel, initialProvider, messages, isStre
|
|
|
362
380
|
},
|
|
363
381
|
]);
|
|
364
382
|
}
|
|
383
|
+
else if (ev.kind === "redirect_received") {
|
|
384
|
+
// Hermes Gap 2 — UI breadcrumb so user sees the directive
|
|
385
|
+
// landed. The loop already appended it to context; we
|
|
386
|
+
// ONLY render here (do NOT push to context again).
|
|
387
|
+
for (const message of ev.messages) {
|
|
388
|
+
setMessages((prev) => [
|
|
389
|
+
...prev,
|
|
390
|
+
{
|
|
391
|
+
role: "system",
|
|
392
|
+
content: `↳ redirect queued: ${message}`,
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
365
397
|
continue;
|
|
366
398
|
}
|
|
367
399
|
if (ev.type === "text" && ev.content.length > 0) {
|