@yanlinglabs/winter-agent-runtime 0.0.27 → 0.0.29
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 +7 -1
- package/dist/context/attachments.d.ts +37 -0
- package/dist/context/seam.d.ts +19 -1
- package/dist/embedded-host.d.ts +14 -0
- package/dist/embedded-host.js +11 -1
- package/dist/embedded-protocol.d.ts +17 -1
- package/dist/embedded-worker.js +20 -3
- package/dist/embedded.js +4 -3
- package/dist/engine.d.ts +23 -0
- package/dist/hooks/additional-context.d.ts +20 -0
- package/dist/hooks/async-hooks.d.ts +64 -0
- package/dist/hooks/command-invoker.d.ts +8 -1
- package/dist/hooks/registry.d.ts +6 -0
- package/dist/hooks/runner.d.ts +22 -0
- package/dist/index-8jgwp1px.js +533 -0
- package/dist/{index-rkhh0457.js → index-eyddzcgs.js} +2 -2
- package/dist/{index-584yahed.js → index-gmxxtqpe.js} +176 -107
- package/dist/{index-9qgkpv56.js → index-jz5d90c6.js} +604 -587
- package/dist/index.js +2 -1
- package/dist/mcp/client.d.ts +16 -1
- package/dist/mcp/test-fixtures.d.ts +2 -0
- package/dist/mcp/transports/stdio.d.ts +19 -0
- package/dist/mcp-client.d.ts +28 -0
- package/dist/mcp-client.js +23 -0
- package/dist/permissions/paths.d.ts +5 -0
- package/dist/process-groups.d.ts +22 -0
- package/dist/provider/classifier/model-classifier.d.ts +12 -0
- package/dist/store/dialect.d.ts +1 -1
- package/dist/store/provider-state.d.ts +14 -1
- package/dist/subagents/child-handle.d.ts +8 -0
- package/dist/testing.js +3 -2
- package/dist/tools/registry.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -49,9 +49,15 @@ each session needs its own JavaScript realm: run one session per Bun `Worker`.
|
|
|
49
49
|
and returns a `SpawnedRuntimeProcess`. Hand it to `query()` through `Options.spawnClaudeCodeProcess`.
|
|
50
50
|
The frames on the wire are exactly the ones the spawned binary writes. This module does not load
|
|
51
51
|
the engine, so it is safe to import on a host's main thread.
|
|
52
|
+
A session's shell commands, stdio MCP servers and hooks run as their own process groups. A Worker
|
|
53
|
+
that is `terminate()`d or crashes never runs the teardown that kills them, so once `exited` settles,
|
|
54
|
+
SIGKILL every group the returned process still lists in `processGroups()`
|
|
55
|
+
(`process.kill(-pgid, "SIGKILL")`). After a clean exit the list is empty.
|
|
52
56
|
- `@yanlinglabs/winter-agent-runtime/embedded-worker` — the Worker entry. A compiled
|
|
53
57
|
(`bun build --compile`) host passes its own one-line worker file as an extra entrypoint and
|
|
54
|
-
constructs the Worker from that file's plain relative path.
|
|
58
|
+
constructs the Worker from that file's plain relative path. Compile the host with
|
|
59
|
+
`--no-compile-autoload-bunfig --no-compile-autoload-dotenv`, as the `winter` binary is: a session
|
|
60
|
+
runs in a user's repository, and otherwise the binary reads that directory's `bunfig.toml` and `.env`.
|
|
55
61
|
- `@yanlinglabs/winter-agent-runtime/embedded` — `runEmbeddedSession(...)`, one session with every
|
|
56
62
|
process global (argv, env, stdio, exit) as a parameter.
|
|
57
63
|
- `@yanlinglabs/winter-agent-runtime/workflow-worker` — the Workflow tool's sandboxed worker entry,
|
|
@@ -30,6 +30,29 @@ export interface DateChangeAttachment extends AttachmentPayload {
|
|
|
30
30
|
type: "date_change";
|
|
31
31
|
newDate: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* WS-24 (I-1 fix round): Winter's own, no claude equivalent. Plan mode moved OUT of the system
|
|
35
|
+
* prompt's dynamic half and into a persisted attachment at the tail of the conversation -- the block
|
|
36
|
+
* used to sit ahead of the conversation history, so a toggle shifted every downstream token and
|
|
37
|
+
* busted the whole cached prefix on the vendor's own prompt-cache accounting (WS-24 follow-up 8's
|
|
38
|
+
* confirmed live finding, on every provider: OpenAI's byte-exact prefix match and Anthropic's `org`
|
|
39
|
+
* dynamic system block alike). As an attachment it costs exactly ONE cache miss on the turn the mode
|
|
40
|
+
* actually changes, and the history stays a stable, cacheable prefix while the mode holds steady in
|
|
41
|
+
* either direction.
|
|
42
|
+
*/
|
|
43
|
+
export interface PlanModeAttachment extends AttachmentPayload {
|
|
44
|
+
type: "plan_mode";
|
|
45
|
+
state: "entered" | "exited";
|
|
46
|
+
/**
|
|
47
|
+
* Present only for `state: "entered"`. Captured ONCE at production time
|
|
48
|
+
* (`SystemPromptAssembler.planModeInput`, the settings/brand precedence `assemble()` used to apply
|
|
49
|
+
* inline) rather than re-derived at render time -- a renderer takes only the payload, never live
|
|
50
|
+
* settings or the session's brand.
|
|
51
|
+
*/
|
|
52
|
+
plansDirectory?: string;
|
|
53
|
+
plansDirectoryFallback?: string;
|
|
54
|
+
hostPlanBody?: string;
|
|
55
|
+
}
|
|
33
56
|
export declare const AGENT_LISTING_INITIAL_HEADER = "Available agent types for the Agent tool:";
|
|
34
57
|
export declare const AGENT_LISTING_ADDED_HEADER = "New agent types are now available for the Agent tool:";
|
|
35
58
|
export declare const AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
|
|
@@ -39,6 +62,13 @@ export declare const AMBIENT_CONTEXT_SENTENCE = "This is ambient context \u2014
|
|
|
39
62
|
export declare const AGENT_CONCURRENCY_SENTENCE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
|
|
40
63
|
export declare const SKILL_LISTING_HEADER = "The following skills are available for use with the Skill tool:";
|
|
41
64
|
export declare function dateChangeText(newDate: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* WS-24 (I-1): kept deliberately MINIMAL. The `ExitPlanMode` tool's own result already announces the
|
|
67
|
+
* mode change in the model-visible function_call_output ("Plan approved; permission mode restored to
|
|
68
|
+
* ..."), so this attachment's job is only to cover the OTHER way the mode can leave plan -- a host or
|
|
69
|
+
* UI action (`set_permission_mode`) with no tool call at all -- without repeating that sentence.
|
|
70
|
+
*/
|
|
71
|
+
export declare const PLAN_MODE_EXITED_TEXT = "Plan mode has ended. The write restriction is lifted.";
|
|
42
72
|
/** claude's attachment wrapper (`Qa`). Nothing is added around it and nothing after it. */
|
|
43
73
|
export declare function wrapSystemReminder(body: string): string;
|
|
44
74
|
/** A renderer returns the UNWRAPPED body, or `undefined` when the attachment has nothing to say. */
|
|
@@ -91,6 +121,13 @@ export declare function attachmentsIn(messages: readonly ProviderMessage[]): Att
|
|
|
91
121
|
export declare function announcedAgentTypes(messages: readonly ProviderMessage[]): Set<string>;
|
|
92
122
|
/** claude's `alr` fold: whether a `date_change` for `date` is already in the history. */
|
|
93
123
|
export declare function dateChangeAnnounced(messages: readonly ProviderMessage[], date: string): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* WS-24 (I-1): the last `plan_mode` attachment's state, or `"exited"` when none exists yet -- a
|
|
126
|
+
* session that never entered plan mode is, correctly, not IN it. This is what the engine's own
|
|
127
|
+
* producer compares against the LIVE `policyStateStore` mode to decide whether anything changed
|
|
128
|
+
* since the history's own last word on it -- emitting only on a genuine difference, never every turn.
|
|
129
|
+
*/
|
|
130
|
+
export declare function lastPlanModeState(messages: readonly ProviderMessage[]): "entered" | "exited";
|
|
94
131
|
/**
|
|
95
132
|
* claude's `vlr` resume seed for the skill listing: the names every persisted `skill_listing` sent,
|
|
96
133
|
* and whether a legacy entry without `names` asks the next listing to be suppressed (claude's
|
package/dist/context/seam.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RuntimeConfig } from "@yanlinglabs/winter-agent-sdk";
|
|
2
2
|
import type { ContextEntry } from "./request-layout.js";
|
|
3
|
+
import type { PlanModeInput } from "./plan-mode.js";
|
|
3
4
|
export type SkillListing = Array<{
|
|
4
5
|
name: string;
|
|
5
6
|
description: string;
|
|
@@ -43,7 +44,12 @@ export interface SystemPromptInput {
|
|
|
43
44
|
modelDisplayName?: string;
|
|
44
45
|
memoryDir?: string;
|
|
45
46
|
outputStyle?: OutputStyle;
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* WS-24 (I-1 fix round): the host's replacement for the plan-mode body's middle section
|
|
49
|
+
* (`planModeInstructions` on the wire, `plan-mode.ts`'s own header on the two-name mapping). Still
|
|
50
|
+
* carried on the snapshot -- `planModeInput()` reads it -- even though the block itself no longer
|
|
51
|
+
* renders inline here; it moved to a persisted attachment (`context/attachments.ts`'s `plan_mode`).
|
|
52
|
+
*/
|
|
47
53
|
hostPlanBody?: string;
|
|
48
54
|
/**
|
|
49
55
|
* DISCLOSED WINTER FIELD, not in the brief's list: the child persona a subagent runs with
|
|
@@ -118,6 +124,18 @@ export interface SystemPromptAssembler {
|
|
|
118
124
|
* per session context rather than once per turn. Absent = no index-0 message.
|
|
119
125
|
*/
|
|
120
126
|
userContext?(input: SystemPromptInput): ContextEntry[];
|
|
127
|
+
/**
|
|
128
|
+
* WS-24 (I-1 fix round): the plan-mode block's render inputs -- `plansDirectory` (the session's own
|
|
129
|
+
* project dot-dir default, config then settings then the brand's), `plansDirectoryFallback` (the
|
|
130
|
+
* same brand default, for a refused `plansDirectory`) and `hostPlanBody` passed through. `assemble()`
|
|
131
|
+
* used to derive these inline and render the block into the dynamic system half on every request
|
|
132
|
+
* while the mode held; the block moved to a persisted attachment (`context/attachments.ts`'s
|
|
133
|
+
* `plan_mode`) so a toggle costs one cache miss instead of shifting the whole downstream prefix on
|
|
134
|
+
* every request. The ENGINE calls this ONCE, when producing the attachment on a genuine mode change
|
|
135
|
+
* -- never per render -- so the settings/brand precedence stays derived in this one place. Absent =
|
|
136
|
+
* the engine falls back to the SDK's own `DEFAULT_PLANS_DIRECTORY` with no `hostPlanBody`.
|
|
137
|
+
*/
|
|
138
|
+
planModeInput?(input: SystemPromptInput): PlanModeInput;
|
|
121
139
|
}
|
|
122
140
|
/**
|
|
123
141
|
* The spine's own test double. NOT a minimal prompt and never a stand-in for one (R5-16): it echoes
|
package/dist/embedded-host.d.ts
CHANGED
|
@@ -39,6 +39,20 @@ export interface EmbeddedWorkerProcess extends SpawnedRuntimeProcess {
|
|
|
39
39
|
terminate(): void;
|
|
40
40
|
/** `running` → (`kill()`) `stopping` → `closed`. `closed` means `exited` has settled. */
|
|
41
41
|
readonly state: "running" | "stopping" | "closed";
|
|
42
|
+
/**
|
|
43
|
+
* WS-24: the process groups the session reported live and has not reported ended
|
|
44
|
+
* (`EmbeddedProcessGroupMessage`), oldest first. Still readable once `exited` has settled -- which is
|
|
45
|
+
* when it matters: a Worker that closed WITHOUT its own teardown (`terminate()` of a spinning Worker,
|
|
46
|
+
* a crash) leaves here exactly the groups it orphaned, and nothing but the host can reap them (each
|
|
47
|
+
* was `setsid`-detached). A healthy close leaves it empty, because every removal is posted before
|
|
48
|
+
* `exit`.
|
|
49
|
+
*
|
|
50
|
+
* The host OWNS the reaping, deliberately: SIGKILL `-pgid` for each entry once `exited` settles
|
|
51
|
+
* (Winter's daemon does, in `runtime-sdk/embedded.ts`). Residual: a group that ended in the instant
|
|
52
|
+
* between its last message and the kill leaves a pgid the OS could in principle hand to a new group
|
|
53
|
+
* leader -- pids are not reused while any member of the group lives, so only that race window remains.
|
|
54
|
+
*/
|
|
55
|
+
processGroups(): readonly number[];
|
|
42
56
|
}
|
|
43
57
|
/**
|
|
44
58
|
* Construct the Worker, send `start`, and return the process handle. Never throws for a Worker that
|
package/dist/embedded-host.js
CHANGED
|
@@ -28,6 +28,7 @@ function spawnEmbeddedWorker(opts) {
|
|
|
28
28
|
let stdinEnded = false;
|
|
29
29
|
let graceTimer;
|
|
30
30
|
let closeTimer;
|
|
31
|
+
const processGroups = new Set;
|
|
31
32
|
let settleExited;
|
|
32
33
|
const exited = new Promise((resolve) => {
|
|
33
34
|
settleExited = resolve;
|
|
@@ -77,6 +78,14 @@ function spawnEmbeddedWorker(opts) {
|
|
|
77
78
|
case "stderr":
|
|
78
79
|
stderr.write(message.chunk);
|
|
79
80
|
return;
|
|
81
|
+
case "process-group":
|
|
82
|
+
if (!Number.isInteger(message.pgid) || message.pgid <= 1 || message.pgid === process.pid)
|
|
83
|
+
return;
|
|
84
|
+
if (message.op === "add")
|
|
85
|
+
processGroups.add(message.pgid);
|
|
86
|
+
else
|
|
87
|
+
processGroups.delete(message.pgid);
|
|
88
|
+
return;
|
|
80
89
|
case "exit":
|
|
81
90
|
exitCode = message.code;
|
|
82
91
|
closeTimer = setTimeout(terminate, CLOSE_AFTER_EXIT_MS);
|
|
@@ -146,7 +155,8 @@ function spawnEmbeddedWorker(opts) {
|
|
|
146
155
|
pid: null,
|
|
147
156
|
get state() {
|
|
148
157
|
return state;
|
|
149
|
-
}
|
|
158
|
+
},
|
|
159
|
+
processGroups: () => [...processGroups]
|
|
150
160
|
};
|
|
151
161
|
}
|
|
152
162
|
export {
|
|
@@ -31,10 +31,26 @@ export type EmbeddedWorkerMessage = {
|
|
|
31
31
|
} | {
|
|
32
32
|
kind: "stderr";
|
|
33
33
|
chunk: string;
|
|
34
|
-
} | {
|
|
34
|
+
} | EmbeddedProcessGroupMessage | {
|
|
35
35
|
kind: "exit";
|
|
36
36
|
code: number;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* WS-24: a process group the session's realm started (`add`) or saw end (`remove`) -- Bash/Monitor
|
|
40
|
+
* commands, stdio MCP servers, command hooks, the workflow worker (`process-groups.ts`). The host keeps
|
|
41
|
+
* the live set so that a Worker which dies WITHOUT running its own kill doors (a `terminate()` of a
|
|
42
|
+
* spinning Worker, a crash) does not orphan them: `setsid` detached every one from the host process,
|
|
43
|
+
* so nothing else will ever reap them. Posted in order with `stdout`, so by the time a healthy Worker's
|
|
44
|
+
* `exit` arrives every group its teardown ended has already been removed.
|
|
45
|
+
*
|
|
46
|
+
* ADDITIVE: a host that predates it ignores an unknown `kind` (`embedded-host.ts`'s switch has no
|
|
47
|
+
* default arm), and a runtime that predates it simply never posts one.
|
|
48
|
+
*/
|
|
49
|
+
export interface EmbeddedProcessGroupMessage {
|
|
50
|
+
kind: "process-group";
|
|
51
|
+
op: "add" | "remove";
|
|
52
|
+
pgid: number;
|
|
53
|
+
}
|
|
38
54
|
/**
|
|
39
55
|
* The request ids of the two control frames an ABORT synthesizes (`embedded.ts`). Fixed strings, not
|
|
40
56
|
* UUIDs, so the output filter can recognise their acknowledgements without state: no host ever
|
package/dist/embedded-worker.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import"./index-
|
|
1
|
+
import"./index-jz5d90c6.js";
|
|
2
|
+
import {
|
|
3
|
+
liveProcessGroups,
|
|
4
|
+
onProcessGroupChange
|
|
5
|
+
} from "./index-8jgwp1px.js";
|
|
2
6
|
import"./index-bef62z3r.js";
|
|
3
|
-
import"./index-
|
|
7
|
+
import"./index-gmxxtqpe.js";
|
|
4
8
|
import {
|
|
5
9
|
runEmbeddedSession2
|
|
6
|
-
} from "./index-
|
|
10
|
+
} from "./index-eyddzcgs.js";
|
|
7
11
|
import {
|
|
8
12
|
Queue
|
|
9
13
|
} from "./index-97t2rmtf.js";
|
|
10
14
|
|
|
11
15
|
// src/embedded-worker.ts
|
|
12
16
|
import { isMainThread } from "node:worker_threads";
|
|
17
|
+
var EXIT_AFTER_GROUPS_SETTLE_MS = 250;
|
|
18
|
+
async function groupsSettled() {
|
|
19
|
+
const deadline = Date.now() + EXIT_AFTER_GROUPS_SETTLE_MS;
|
|
20
|
+
while (liveProcessGroups().length > 0 && Date.now() < deadline)
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
22
|
+
}
|
|
13
23
|
function post(message) {
|
|
14
24
|
postMessage(message);
|
|
15
25
|
}
|
|
@@ -24,8 +34,14 @@ function fenceProcessWideState() {
|
|
|
24
34
|
return readUmask();
|
|
25
35
|
};
|
|
26
36
|
}
|
|
37
|
+
function mirrorProcessGroups() {
|
|
38
|
+
for (const { pgid } of liveProcessGroups())
|
|
39
|
+
post({ kind: "process-group", op: "add", pgid });
|
|
40
|
+
onProcessGroupChange((change) => post({ kind: "process-group", op: change.op, pgid: change.pgid }));
|
|
41
|
+
}
|
|
27
42
|
function installEmbeddedWorker() {
|
|
28
43
|
fenceProcessWideState();
|
|
44
|
+
mirrorProcessGroups();
|
|
29
45
|
const input = new Queue;
|
|
30
46
|
const abort = new AbortController;
|
|
31
47
|
let started = false;
|
|
@@ -46,6 +62,7 @@ function installEmbeddedWorker() {
|
|
|
46
62
|
` });
|
|
47
63
|
code = 1;
|
|
48
64
|
}
|
|
65
|
+
await groupsSettled();
|
|
49
66
|
post({ kind: "exit", code });
|
|
50
67
|
process.exit(code);
|
|
51
68
|
};
|
package/dist/embedded.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runEmbeddedSession2
|
|
3
|
-
} from "./index-
|
|
4
|
-
import"./index-
|
|
5
|
-
import"./index-
|
|
3
|
+
} from "./index-eyddzcgs.js";
|
|
4
|
+
import"./index-gmxxtqpe.js";
|
|
5
|
+
import"./index-jz5d90c6.js";
|
|
6
6
|
import"./index-bef62z3r.js";
|
|
7
|
+
import"./index-8jgwp1px.js";
|
|
7
8
|
export {
|
|
8
9
|
runEmbeddedSession2 as runEmbeddedSession
|
|
9
10
|
};
|
package/dist/engine.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export type ContentBlock = {
|
|
|
64
64
|
data: string;
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* WS-24: the text a FORK's ToolSearch result carries for loaded tools its frozen `tools` does not declare
|
|
69
|
+
* (the engine's `forkLoadedDefinitionsText`, on a row with `undeclaredToolCalls` evidence). Exported so
|
|
70
|
+
* the live probe that gathers that evidence (`scripts/probe-fork-undeclared-tool.ts`) sends these exact
|
|
71
|
+
* bytes rather than a lookalike.
|
|
72
|
+
*/
|
|
73
|
+
export declare function undeclaredToolDefinitionsText(definitions: readonly LoadedToolDefinition[]): string;
|
|
67
74
|
/** WS-23 (midconv, review I-2): one loaded tool's definition as it stood at load time (see `tool_result.loadedToolDefinitions`). */
|
|
68
75
|
export interface LoadedToolDefinition {
|
|
69
76
|
name: string;
|
|
@@ -184,6 +191,8 @@ export interface ModelWireFeatures {
|
|
|
184
191
|
additionalToolsItem?: true;
|
|
185
192
|
/** WS-23 (midconv): OpenAI's `tool_choice: allowed_tools` restricts the callable set without editing `tools` (`allowedToolsChoice`). */
|
|
186
193
|
allowedToolsChoice?: true;
|
|
194
|
+
/** WS-24: the endpoint takes a call (and its history) to a tool absent from `tools` (`undeclaredToolCalls`, live-probe-proven) -- what lets a fork run a self-loaded tool its frozen `tools` lacks. */
|
|
195
|
+
undeclaredToolCalls?: true;
|
|
187
196
|
}
|
|
188
197
|
/** What `EngineOptions.describeModel` knows about a model: its display name, its verified effort vocabulary, and its wire features. */
|
|
189
198
|
export interface ModelDescription {
|
|
@@ -458,6 +467,11 @@ export interface ProviderUsage {
|
|
|
458
467
|
};
|
|
459
468
|
/** WS-23: replayed thinking blocks the provider dropped (Anthropic's `input_transformations`). */
|
|
460
469
|
thinkingBlocksDropped?: number;
|
|
470
|
+
/**
|
|
471
|
+
* WS-24 (follow-up 1, lane `providers`): the Responses family's reasoning-token count -- a SUBSET
|
|
472
|
+
* of `outputTokens`. Anthropic reports no separate count and leaves this absent.
|
|
473
|
+
*/
|
|
474
|
+
reasoningTokens?: number;
|
|
461
475
|
}
|
|
462
476
|
export type ProviderTurn = {
|
|
463
477
|
kind: "text";
|
|
@@ -809,6 +823,15 @@ export interface EngineOptions {
|
|
|
809
823
|
* Round 20 carried the parent's board only, and only to a child with object-form servers.
|
|
810
824
|
*/
|
|
811
825
|
inheritedMcpServerNames?: () => readonly string[];
|
|
826
|
+
/**
|
|
827
|
+
* WS-24: a SUBAGENT's own MCP servers connected under a name other than the one its definition
|
|
828
|
+
* declared (subagents/child-engine.ts's `allocateChildScopedServers` renames one that collides),
|
|
829
|
+
* as `{ actual: declared }`. Set by child-engine.ts only. A call to `mcp__<actual>__<tool>` is then
|
|
830
|
+
* ALSO governed by every permission rule and hook matcher written against `mcp__<declared>__<tool>`
|
|
831
|
+
* (strictest-of, like a tool alias), so a rename never lets a call escape a rule or hook that named
|
|
832
|
+
* the server as the author declared it.
|
|
833
|
+
*/
|
|
834
|
+
mcpServerRenames?: Readonly<Record<string, string>>;
|
|
812
835
|
mcpControlSeam?: McpControlSeam;
|
|
813
836
|
contextAccountant?: ContextAccountant;
|
|
814
837
|
onChildRosterReady?: (getChildren: () => readonly ChildHandle[]) => void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AttachmentPayload } from "../context/attachments.js";
|
|
2
2
|
export declare const HOOK_ADDITIONAL_CONTEXT_ATTACHMENT = "hook_additional_context";
|
|
3
3
|
export declare const HOOK_FEEDBACK_ATTACHMENT = "hook_feedback";
|
|
4
|
+
/** WS-24: what a BACKGROUND (async) hook said once it finished -- hooks/async-hooks.ts. */
|
|
5
|
+
export declare const ASYNC_HOOK_RESPONSE_ATTACHMENT = "async_hook_response";
|
|
4
6
|
export interface HookAdditionalContextAttachment extends AttachmentPayload {
|
|
5
7
|
type: typeof HOOK_ADDITIONAL_CONTEXT_ATTACHMENT;
|
|
6
8
|
hookName: string;
|
|
@@ -12,6 +14,24 @@ export interface HookFeedbackAttachment extends AttachmentPayload {
|
|
|
12
14
|
hookName: string;
|
|
13
15
|
content: string[];
|
|
14
16
|
}
|
|
17
|
+
export interface AsyncHookResponseAttachment extends AttachmentPayload {
|
|
18
|
+
type: typeof ASYNC_HOOK_RESPONSE_ATTACHMENT;
|
|
19
|
+
hookName: string;
|
|
20
|
+
systemMessage?: string;
|
|
21
|
+
content: string[];
|
|
22
|
+
toolUseID?: string;
|
|
23
|
+
/** Set on the notice that `dropped` finished outputs never reached the model (the pending cap). */
|
|
24
|
+
dropped?: number;
|
|
25
|
+
}
|
|
26
|
+
/** WS-24: the attachment for one finished async hook -- `undefined` when it said nothing. */
|
|
27
|
+
export declare function asyncHookResponseAttachment(output: {
|
|
28
|
+
hookName: string;
|
|
29
|
+
systemMessage?: string;
|
|
30
|
+
additionalContext?: string;
|
|
31
|
+
toolUseID?: string;
|
|
32
|
+
}): AsyncHookResponseAttachment | undefined;
|
|
33
|
+
/** WS-24: the notice that `dropped` finished async-hook outputs were discarded for the pending cap. */
|
|
34
|
+
export declare function asyncHookDroppedAttachment(dropped: number): AsyncHookResponseAttachment | undefined;
|
|
15
35
|
/** The `additionalContext` strings a composite accumulated, in evaluation order (non-string or empty entries dropped). */
|
|
16
36
|
export declare function contextStrings(extraContext: ReadonlyArray<{
|
|
17
37
|
context: unknown;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { HookEvent } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
/** Ten minutes: background work (a test run after an edit, a lint of the tree) is minutes, not seconds -- the gating hooks' 60 s would cut it off. */
|
|
3
|
+
export declare const DEFAULT_ASYNC_HOOK_TIMEOUT_MS = 600000;
|
|
4
|
+
/**
|
|
5
|
+
* Concurrent background hooks per ENGINE. A PostToolUse hook on every call of a 50-call burst must not
|
|
6
|
+
* become 50 processes. Per engine, not per session: a subagent's engine builds its own command invoker
|
|
7
|
+
* and so its own queue (killed at that engine's teardown), so a session running N subagents at once can
|
|
8
|
+
* hold up to (N + 1) x this many -- each still bounded, and each gone with its engine.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_RUNNING_ASYNC_HOOKS = 16;
|
|
11
|
+
/** Finished outputs waiting for the next safe point. At `MAX_HOOK_TEXT_CHARS` each, one delivery stays far below the per-message cap. */
|
|
12
|
+
export declare const MAX_PENDING_ASYNC_HOOK_OUTPUTS = 16;
|
|
13
|
+
/** One finished async hook's model-facing output. */
|
|
14
|
+
export interface AsyncHookOutput {
|
|
15
|
+
/** `<Event>` or `<Event>:<tool name>` -- the same naming the synchronous context attachments use. */
|
|
16
|
+
hookName: string;
|
|
17
|
+
systemMessage?: string;
|
|
18
|
+
additionalContext?: string;
|
|
19
|
+
toolUseID?: string;
|
|
20
|
+
}
|
|
21
|
+
/** What `drain()` returns: finished outputs, oldest first, plus how many were dropped for the pending cap. */
|
|
22
|
+
export interface AsyncHookDrain {
|
|
23
|
+
outputs: AsyncHookOutput[];
|
|
24
|
+
dropped: number;
|
|
25
|
+
}
|
|
26
|
+
/** A backgrounded invocation as the command invoker hands it over. */
|
|
27
|
+
export interface AsyncHookJob {
|
|
28
|
+
event: HookEvent;
|
|
29
|
+
hookName: string;
|
|
30
|
+
toolUseID?: string;
|
|
31
|
+
/** Settles when the process has exited, with what `finishedOutput` needs. Never rejects. */
|
|
32
|
+
finished: Promise<FinishedHookProcess>;
|
|
33
|
+
/** SIGKILL the job's whole process group. Idempotent. */
|
|
34
|
+
kill: () => void;
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
}
|
|
37
|
+
/** A background process's end state. */
|
|
38
|
+
export interface FinishedHookProcess {
|
|
39
|
+
exitCode: number | null;
|
|
40
|
+
stdout: string;
|
|
41
|
+
}
|
|
42
|
+
export interface AsyncHookQueue {
|
|
43
|
+
/** Take ownership of a backgrounded job. `false` (and the job killed) when the concurrency cap is reached or the queue is disposed. */
|
|
44
|
+
adopt(job: AsyncHookJob): boolean;
|
|
45
|
+
/** Finished outputs since the last drain, oldest first. Empties the queue. */
|
|
46
|
+
drain(): AsyncHookDrain;
|
|
47
|
+
/** Jobs still running. */
|
|
48
|
+
running(): number;
|
|
49
|
+
/** Session end: kill every running job, forget every undelivered output. Idempotent. */
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What a finished background hook SAYS -- `systemMessage` and `additionalContext`, nothing else (see this
|
|
54
|
+
* file's header). Only a clean exit counts: a non-zero exit (2 included -- a block it can no longer
|
|
55
|
+
* perform) or a kill says nothing. `stdout` is everything AFTER an announced `{"async": true}` line.
|
|
56
|
+
*/
|
|
57
|
+
export declare function finishedOutput(event: HookEvent, finished: FinishedHookProcess): Pick<AsyncHookOutput, "systemMessage" | "additionalContext">;
|
|
58
|
+
export interface AsyncHookQueueOptions {
|
|
59
|
+
maxRunning?: number;
|
|
60
|
+
maxPending?: number;
|
|
61
|
+
/** Where a refused, failed or timed-out job is reported (one line each). Defaults to stderr. */
|
|
62
|
+
warn?: (line: string) => void;
|
|
63
|
+
}
|
|
64
|
+
export declare function createAsyncHookQueue(opts?: AsyncHookQueueOptions): AsyncHookQueue;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
2
2
|
import { type HookInvocationRequest, type HookInvoker } from "./runner.js";
|
|
3
3
|
import type { SourcedHookEntry } from "./registry.js";
|
|
4
|
+
import { type AsyncHookQueue } from "./async-hooks.js";
|
|
4
5
|
/** Grace between SIGTERM and SIGKILL. Short: by the time this fires the runner has already given up on the hook. */
|
|
5
6
|
export declare const COMMAND_HOOK_KILL_GRACE_MS = 250;
|
|
6
7
|
export declare class CommandHookError extends Error {
|
|
@@ -28,6 +29,12 @@ export interface CommandHookInvokerOptions {
|
|
|
28
29
|
transcriptPath?: string;
|
|
29
30
|
/** WS-23: the stdin input's `permission_mode`, read at invocation time (it changes mid-session). */
|
|
30
31
|
permissionMode?: () => string | undefined;
|
|
32
|
+
/** WS-24: where backgrounded (async) hooks go. Defaults to a fresh `createAsyncHookQueue()`, exposed as the invoker's `asyncHooks`. */
|
|
33
|
+
asyncHooks?: AsyncHookQueue;
|
|
34
|
+
}
|
|
35
|
+
/** WS-24: the command invoker also OWNS the session's background hooks (hooks/async-hooks.ts). */
|
|
36
|
+
export interface CommandHookInvoker extends HookInvoker {
|
|
37
|
+
readonly asyncHooks: AsyncHookQueue;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Wraps `opts.next`, executing any invocation whose `hookId` belongs to a command-bearing entry as a
|
|
@@ -36,7 +43,7 @@ export interface CommandHookInvokerOptions {
|
|
|
36
43
|
* The entry list is snapshotted at construction, matching `buildHookRegistry`'s own "a registry is
|
|
37
44
|
* immutable for the life of a run" contract -- a run builds both from the same entries.
|
|
38
45
|
*/
|
|
39
|
-
export declare function createCommandHookInvoker(entries: readonly SourcedHookEntry[], opts: CommandHookInvokerOptions):
|
|
46
|
+
export declare function createCommandHookInvoker(entries: readonly SourcedHookEntry[], opts: CommandHookInvokerOptions): CommandHookInvoker;
|
|
40
47
|
/**
|
|
41
48
|
* claude's command-hook stdin: the snake_case `HookInput` for this event, built from the runner's
|
|
42
49
|
* camelCase request. `payload` already carries each event's own fields in claude's spelling (the
|
package/dist/hooks/registry.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface SourcedHookEntry extends HookParticipant {
|
|
|
21
21
|
* (command-invoker.ts). Absent on every non-plugin entry.
|
|
22
22
|
*/
|
|
23
23
|
pluginRoot?: string;
|
|
24
|
+
/**
|
|
25
|
+
* WS-24: a settings/plugin command handler declared `async: true` -- it runs in the BACKGROUND and
|
|
26
|
+
* never blocks its event (hooks/async-hooks.ts). Never set together with `failClosed` on a
|
|
27
|
+
* PreToolUse/PermissionRequest entry: from-config.ts refuses the flag there (a floor must gate).
|
|
28
|
+
*/
|
|
29
|
+
async?: boolean;
|
|
24
30
|
}
|
|
25
31
|
export interface HookRegistry {
|
|
26
32
|
matching(event: HookEvent, toolName?: string): SourcedHookEntry[];
|
package/dist/hooks/runner.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HookEvent, HookPermissionDecision } from "@yanlinglabs/winter-agent-sdk";
|
|
2
2
|
import { type HookRegistry } from "./registry.js";
|
|
3
3
|
import { type HookComposite } from "./reducer.js";
|
|
4
|
+
import type { AsyncHookQueue } from "./async-hooks.js";
|
|
4
5
|
export interface HookInvocationRequest {
|
|
5
6
|
event: HookEvent;
|
|
6
7
|
matchedMatcher?: string;
|
|
@@ -8,6 +9,14 @@ export interface HookInvocationRequest {
|
|
|
8
9
|
agentID?: string;
|
|
9
10
|
toolUseID?: string;
|
|
10
11
|
toolName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* WS-24: for an MCP tool, the server that registered `toolName` and the tool's own name there
|
|
14
|
+
* (`mcpToolProvenance` below). Both builders of a hook's input -- `commandHookInput`
|
|
15
|
+
* (command-invoker.ts) and the wrapper's `buildHookInput` (sdk query.ts) -- turn them into
|
|
16
|
+
* `mcp_server_name`/`mcp_tool_name`. Absent for every non-MCP tool and every tool-less event.
|
|
17
|
+
*/
|
|
18
|
+
mcpServerName?: string;
|
|
19
|
+
mcpToolName?: string;
|
|
11
20
|
input?: Record<string, unknown>;
|
|
12
21
|
payload?: unknown;
|
|
13
22
|
policyVersion: string;
|
|
@@ -15,10 +24,21 @@ export interface HookInvocationRequest {
|
|
|
15
24
|
hookId: string;
|
|
16
25
|
hookName?: string;
|
|
17
26
|
}
|
|
27
|
+
export interface McpToolProvenanceInfo {
|
|
28
|
+
server: string;
|
|
29
|
+
tool: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function mcpToolProvenance(toolName: string): McpToolProvenanceInfo | undefined;
|
|
18
32
|
export interface HookInvoker {
|
|
19
33
|
invoke(request: HookInvocationRequest, opts: {
|
|
20
34
|
signal: AbortSignal;
|
|
21
35
|
}): Promise<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* WS-24: the session's background (async) hooks, when this invoker can run any -- only the command
|
|
38
|
+
* invoker can (hooks/async-hooks.ts). The engine drains it at each safe point and disposes it at
|
|
39
|
+
* session end; a callback-only session has none.
|
|
40
|
+
*/
|
|
41
|
+
readonly asyncHooks?: AsyncHookQueue;
|
|
22
42
|
}
|
|
23
43
|
export type HookAuditOutcome = "decision" | "none" | "error" | "timeout" | "skipped";
|
|
24
44
|
export interface HookAuditRecord {
|
|
@@ -100,6 +120,8 @@ export interface RunHooksContext {
|
|
|
100
120
|
timeouts?: HookTimeoutConfig;
|
|
101
121
|
validator?: ToolInputValidator;
|
|
102
122
|
lifecycle?: HookLifecycleSink;
|
|
123
|
+
/** WS-24: the MCP provenance lookup. Defaults to `mcpToolProvenance` (the live registry); a test seam. */
|
|
124
|
+
mcpProvenance?: (toolName: string) => McpToolProvenanceInfo | undefined;
|
|
103
125
|
}
|
|
104
126
|
export declare const FAIL_CLOSED_EVENTS: ReadonlySet<HookEvent>;
|
|
105
127
|
export declare function runHooks(event: HookEvent, call: RunHooksCallInfo, ctx: RunHooksContext): Promise<HookComposite>;
|