@nubemclaw/agent-events 1.2.2
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/dist/agent-events.d.ts +136 -0
- package/dist/agent-events.d.ts.map +1 -0
- package/dist/agent-events.js +187 -0
- package/dist/agent-events.js.map +1 -0
- package/dist/global-singleton.d.ts +10 -0
- package/dist/global-singleton.d.ts.map +1 -0
- package/dist/global-singleton.js +20 -0
- package/dist/global-singleton.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/listeners.d.ts +10 -0
- package/dist/listeners.d.ts.map +1 -0
- package/dist/listeners.js +24 -0
- package/dist/listeners.js.map +1 -0
- package/package.json +23 -0
- package/src/agent-events.test.ts +356 -0
- package/src/agent-events.ts +355 -0
- package/src/global-singleton.ts +20 -0
- package/src/index.ts +45 -0
- package/src/listeners.ts +30 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbose level (port verbatim OpenClaw `auto-reply/thinking.shared.ts:18`).
|
|
3
|
+
*
|
|
4
|
+
* `off` silences agent commentary; `on` emits standard logs;
|
|
5
|
+
* `full` includes diagnostic detail. Surfaced here so run contexts
|
|
6
|
+
* carry the level without coupling agent-events to a thinking module.
|
|
7
|
+
*/
|
|
8
|
+
export type VerboseLevel = "off" | "on" | "full";
|
|
9
|
+
export type AgentEventStream = "lifecycle" | "tool" | "assistant" | "error" | "item" | "plan" | "approval" | "command_output" | "patch" | "compaction" | "thinking" | (string & {});
|
|
10
|
+
export type AgentItemEventPhase = "start" | "update" | "end";
|
|
11
|
+
export type AgentItemEventStatus = "running" | "completed" | "failed" | "blocked";
|
|
12
|
+
export type AgentItemEventKind = "tool" | "command" | "patch" | "search" | "analysis" | (string & {});
|
|
13
|
+
export type AgentItemEventData = {
|
|
14
|
+
itemId: string;
|
|
15
|
+
phase: AgentItemEventPhase;
|
|
16
|
+
kind: AgentItemEventKind;
|
|
17
|
+
title: string;
|
|
18
|
+
status: AgentItemEventStatus;
|
|
19
|
+
name?: string;
|
|
20
|
+
meta?: string;
|
|
21
|
+
toolCallId?: string;
|
|
22
|
+
startedAt?: number;
|
|
23
|
+
endedAt?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
progressText?: string;
|
|
27
|
+
/** Preserve item telemetry while letting channel progress render a sibling tool event instead. */
|
|
28
|
+
suppressChannelProgress?: boolean;
|
|
29
|
+
approvalId?: string;
|
|
30
|
+
approvalSlug?: string;
|
|
31
|
+
};
|
|
32
|
+
export type AgentPlanEventData = {
|
|
33
|
+
phase: "update";
|
|
34
|
+
title: string;
|
|
35
|
+
explanation?: string;
|
|
36
|
+
steps?: string[];
|
|
37
|
+
source?: string;
|
|
38
|
+
};
|
|
39
|
+
export type AgentApprovalEventPhase = "requested" | "resolved";
|
|
40
|
+
export type AgentApprovalEventStatus = "pending" | "unavailable" | "approved" | "denied" | "failed";
|
|
41
|
+
export type AgentApprovalEventKind = "exec" | "plugin" | "unknown";
|
|
42
|
+
export type AgentApprovalEventData = {
|
|
43
|
+
phase: AgentApprovalEventPhase;
|
|
44
|
+
kind: AgentApprovalEventKind;
|
|
45
|
+
status: AgentApprovalEventStatus;
|
|
46
|
+
title: string;
|
|
47
|
+
itemId?: string;
|
|
48
|
+
toolCallId?: string;
|
|
49
|
+
approvalId?: string;
|
|
50
|
+
approvalSlug?: string;
|
|
51
|
+
command?: string;
|
|
52
|
+
host?: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
scope?: "turn" | "session";
|
|
55
|
+
message?: string;
|
|
56
|
+
};
|
|
57
|
+
export type AgentCommandOutputEventData = {
|
|
58
|
+
itemId: string;
|
|
59
|
+
phase: "delta" | "end";
|
|
60
|
+
title: string;
|
|
61
|
+
toolCallId: string;
|
|
62
|
+
name?: string;
|
|
63
|
+
output?: string;
|
|
64
|
+
status?: AgentItemEventStatus | "running";
|
|
65
|
+
exitCode?: number | null;
|
|
66
|
+
durationMs?: number;
|
|
67
|
+
cwd?: string;
|
|
68
|
+
};
|
|
69
|
+
export type AgentPatchSummaryEventData = {
|
|
70
|
+
itemId: string;
|
|
71
|
+
phase: "end";
|
|
72
|
+
title: string;
|
|
73
|
+
toolCallId: string;
|
|
74
|
+
name?: string;
|
|
75
|
+
added: string[];
|
|
76
|
+
modified: string[];
|
|
77
|
+
deleted: string[];
|
|
78
|
+
summary: string;
|
|
79
|
+
};
|
|
80
|
+
export type AgentEventPayload = {
|
|
81
|
+
runId: string;
|
|
82
|
+
seq: number;
|
|
83
|
+
stream: AgentEventStream;
|
|
84
|
+
ts: number;
|
|
85
|
+
data: Record<string, unknown>;
|
|
86
|
+
sessionKey?: string;
|
|
87
|
+
};
|
|
88
|
+
export type AgentRunContext = {
|
|
89
|
+
sessionKey?: string;
|
|
90
|
+
verboseLevel?: VerboseLevel;
|
|
91
|
+
isHeartbeat?: boolean;
|
|
92
|
+
/** Whether control UI clients should receive chat/agent updates for this run. */
|
|
93
|
+
isControlUiVisible?: boolean;
|
|
94
|
+
/** Timestamp when this context was first registered (for TTL-based cleanup). */
|
|
95
|
+
registeredAt?: number;
|
|
96
|
+
/** Timestamp of last activity (updated on every emitAgentEvent). */
|
|
97
|
+
lastActiveAt?: number;
|
|
98
|
+
};
|
|
99
|
+
export declare function registerAgentRunContext(runId: string, context: AgentRunContext): void;
|
|
100
|
+
export declare function getAgentRunContext(runId: string): AgentRunContext | undefined;
|
|
101
|
+
export declare function clearAgentRunContext(runId: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Sweep stale run contexts that exceeded the given TTL.
|
|
104
|
+
* Guards against orphaned entries when lifecycle "end"/"error" events are missed.
|
|
105
|
+
*/
|
|
106
|
+
export declare function sweepStaleRunContexts(maxAgeMs?: number): number;
|
|
107
|
+
export declare function resetAgentRunContextForTest(): void;
|
|
108
|
+
export declare function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">): void;
|
|
109
|
+
export declare function emitAgentItemEvent(params: {
|
|
110
|
+
runId: string;
|
|
111
|
+
data: AgentItemEventData;
|
|
112
|
+
sessionKey?: string;
|
|
113
|
+
}): void;
|
|
114
|
+
export declare function emitAgentPlanEvent(params: {
|
|
115
|
+
runId: string;
|
|
116
|
+
data: AgentPlanEventData;
|
|
117
|
+
sessionKey?: string;
|
|
118
|
+
}): void;
|
|
119
|
+
export declare function emitAgentApprovalEvent(params: {
|
|
120
|
+
runId: string;
|
|
121
|
+
data: AgentApprovalEventData;
|
|
122
|
+
sessionKey?: string;
|
|
123
|
+
}): void;
|
|
124
|
+
export declare function emitAgentCommandOutputEvent(params: {
|
|
125
|
+
runId: string;
|
|
126
|
+
data: AgentCommandOutputEventData;
|
|
127
|
+
sessionKey?: string;
|
|
128
|
+
}): void;
|
|
129
|
+
export declare function emitAgentPatchSummaryEvent(params: {
|
|
130
|
+
runId: string;
|
|
131
|
+
data: AgentPatchSummaryEventData;
|
|
132
|
+
sessionKey?: string;
|
|
133
|
+
}): void;
|
|
134
|
+
export declare function onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void;
|
|
135
|
+
export declare function resetAgentEventsForTest(): void;
|
|
136
|
+
//# sourceMappingURL=agent-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-events.d.ts","sourceRoot":"","sources":["../src/agent-events.ts"],"names":[],"mappings":"AAkCA;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,gBAAgB,GACxB,WAAW,GACX,MAAM,GACN,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,UAAU,GACV,gBAAgB,GAChB,OAAO,GACP,YAAY,GACZ,UAAU,GACV,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAC7D,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAClF,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,SAAS,GACT,OAAO,GACP,QAAQ,GACR,UAAU,GACV,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,CAAC;IAC3B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG,UAAU,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AACpG,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnE,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,IAAI,EAAE,sBAAsB,CAAC;IAC7B,MAAM,EAAE,wBAAwB,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,gBAAgB,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAgCrF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE7E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,SAAiB,GAAG,MAAM,CAgBvE;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAIlD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,iBAAiB,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG,IAAI,CA4BjF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,kBAAkB,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAOP;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,kBAAkB,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAOP;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,sBAAsB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAOP;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,2BAA2B,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAOP;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,0BAA0B,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAOP;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAGnF;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAK9C"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-events — port verbatim de
|
|
3
|
+
* `~/Desktop/GitHub/openclaw/src/infra/agent-events.ts:1-312`.
|
|
4
|
+
*
|
|
5
|
+
* In-process event bus para agent lifecycle/tool/assistant/etc. La
|
|
6
|
+
* elección del singleton mediante `globalThis[Symbol.for(...)]` (no
|
|
7
|
+
* un módulo-state local) sobrevive a chunks de runtime split (ESM
|
|
8
|
+
* bundles, vitest worker pools, sub-process forks que se re-importan
|
|
9
|
+
* el módulo). El contrato observable es el corazón de cómo TUI
|
|
10
|
+
* embedded, gateway HTTP/WS y daemon se "atan" sin acoplarse — todos
|
|
11
|
+
* los runners suben el mismo bus, todos los listeners ven el mismo
|
|
12
|
+
* stream.
|
|
13
|
+
*
|
|
14
|
+
* Cambios mínimos frente al verbatim OpenClaw:
|
|
15
|
+
*
|
|
16
|
+
* 1. `Symbol.for("openclaw.agentEvents.state")` →
|
|
17
|
+
* `Symbol.for("nubemclaw.agentEvents.state")` por la regla de
|
|
18
|
+
* rebrand canónico ADR-0003 (la palabra "openclaw" no aparece
|
|
19
|
+
* en código v3 fuera de comentarios de ingeniería inversa).
|
|
20
|
+
* 2. `VerboseLevel` inline (declarado aquí, no importado de un
|
|
21
|
+
* módulo `auto-reply/thinking.shared.ts` que v3 no tiene).
|
|
22
|
+
* Tomado verbatim de OpenClaw thinking.shared.ts:18.
|
|
23
|
+
* 3. `emitAgentEvent` construye `enriched` con spread condicional
|
|
24
|
+
* cuando `sessionKey === undefined`, porque v3 usa
|
|
25
|
+
* `exactOptionalPropertyTypes: true` y no permite asignar
|
|
26
|
+
* `undefined` a un campo `?: string`. Shape semantics idénticas.
|
|
27
|
+
* 4. Imports relativos con extensión `.js` (NodeNext ESM v3).
|
|
28
|
+
*
|
|
29
|
+
* El resto (símbolos, line shape, comentarios sustanciales,
|
|
30
|
+
* orden de funciones) se preserva 1:1.
|
|
31
|
+
*/
|
|
32
|
+
import { resolveGlobalSingleton } from "./global-singleton.js";
|
|
33
|
+
import { notifyListeners, registerListener } from "./listeners.js";
|
|
34
|
+
const AGENT_EVENT_STATE_KEY = Symbol.for("nubemclaw.agentEvents.state");
|
|
35
|
+
function getAgentEventState() {
|
|
36
|
+
return resolveGlobalSingleton(AGENT_EVENT_STATE_KEY, () => ({
|
|
37
|
+
seqByRun: new Map(),
|
|
38
|
+
listeners: new Set(),
|
|
39
|
+
runContextById: new Map(),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export function registerAgentRunContext(runId, context) {
|
|
43
|
+
if (!runId) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const state = getAgentEventState();
|
|
47
|
+
const existing = state.runContextById.get(runId);
|
|
48
|
+
if (!existing) {
|
|
49
|
+
const registered = {
|
|
50
|
+
...context,
|
|
51
|
+
registeredAt: context.registeredAt ?? Date.now(),
|
|
52
|
+
};
|
|
53
|
+
state.runContextById.set(runId, registered);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (context.sessionKey !== undefined && existing.sessionKey !== context.sessionKey) {
|
|
57
|
+
existing.sessionKey = context.sessionKey;
|
|
58
|
+
}
|
|
59
|
+
if (context.verboseLevel !== undefined && existing.verboseLevel !== context.verboseLevel) {
|
|
60
|
+
existing.verboseLevel = context.verboseLevel;
|
|
61
|
+
}
|
|
62
|
+
if (context.isControlUiVisible !== undefined) {
|
|
63
|
+
existing.isControlUiVisible = context.isControlUiVisible;
|
|
64
|
+
}
|
|
65
|
+
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
|
66
|
+
existing.isHeartbeat = context.isHeartbeat;
|
|
67
|
+
}
|
|
68
|
+
if (context.registeredAt !== undefined) {
|
|
69
|
+
existing.registeredAt = context.registeredAt;
|
|
70
|
+
}
|
|
71
|
+
if (context.lastActiveAt !== undefined) {
|
|
72
|
+
existing.lastActiveAt = context.lastActiveAt;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function getAgentRunContext(runId) {
|
|
76
|
+
return getAgentEventState().runContextById.get(runId);
|
|
77
|
+
}
|
|
78
|
+
export function clearAgentRunContext(runId) {
|
|
79
|
+
const state = getAgentEventState();
|
|
80
|
+
state.runContextById.delete(runId);
|
|
81
|
+
state.seqByRun.delete(runId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sweep stale run contexts that exceeded the given TTL.
|
|
85
|
+
* Guards against orphaned entries when lifecycle "end"/"error" events are missed.
|
|
86
|
+
*/
|
|
87
|
+
export function sweepStaleRunContexts(maxAgeMs = 30 * 60 * 1000) {
|
|
88
|
+
const state = getAgentEventState();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
let swept = 0;
|
|
91
|
+
for (const [runId, ctx] of state.runContextById.entries()) {
|
|
92
|
+
// Use lastActiveAt (refreshed on every event) to avoid sweeping active runs.
|
|
93
|
+
// Fall back to registeredAt, then treat missing timestamps as infinitely old.
|
|
94
|
+
const lastSeen = ctx.lastActiveAt ?? ctx.registeredAt;
|
|
95
|
+
const age = lastSeen ? now - lastSeen : Infinity;
|
|
96
|
+
if (age > maxAgeMs) {
|
|
97
|
+
state.runContextById.delete(runId);
|
|
98
|
+
state.seqByRun.delete(runId);
|
|
99
|
+
swept++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return swept;
|
|
103
|
+
}
|
|
104
|
+
export function resetAgentRunContextForTest() {
|
|
105
|
+
const state = getAgentEventState();
|
|
106
|
+
state.runContextById.clear();
|
|
107
|
+
state.seqByRun.clear();
|
|
108
|
+
}
|
|
109
|
+
export function emitAgentEvent(event) {
|
|
110
|
+
const state = getAgentEventState();
|
|
111
|
+
const nextSeq = (state.seqByRun.get(event.runId) ?? 0) + 1;
|
|
112
|
+
state.seqByRun.set(event.runId, nextSeq);
|
|
113
|
+
const context = state.runContextById.get(event.runId);
|
|
114
|
+
if (context) {
|
|
115
|
+
context.lastActiveAt = Date.now();
|
|
116
|
+
}
|
|
117
|
+
const isControlUiVisible = context?.isControlUiVisible ?? true;
|
|
118
|
+
const eventSessionKey = typeof event.sessionKey === "string" && event.sessionKey.trim() ? event.sessionKey : undefined;
|
|
119
|
+
// Hidden channel-routed runs should not leak live assistant/tool traffic into
|
|
120
|
+
// Control UI, but lifecycle events still need the session key so gateway
|
|
121
|
+
// listeners can persist terminal session state even if run-context lookup is
|
|
122
|
+
// unavailable by the time the terminal event arrives. Terminal failures are
|
|
123
|
+
// emitted on the lifecycle stream with `phase: "error"`; the separate error
|
|
124
|
+
// stream remains redacted for hidden runs because it is observational only.
|
|
125
|
+
const preserveSessionKey = isControlUiVisible || event.stream === "lifecycle";
|
|
126
|
+
const sessionKey = preserveSessionKey ? (eventSessionKey ?? context?.sessionKey) : undefined;
|
|
127
|
+
const enriched = {
|
|
128
|
+
runId: event.runId,
|
|
129
|
+
stream: event.stream,
|
|
130
|
+
data: event.data,
|
|
131
|
+
seq: nextSeq,
|
|
132
|
+
ts: Date.now(),
|
|
133
|
+
...(sessionKey !== undefined ? { sessionKey } : {}),
|
|
134
|
+
};
|
|
135
|
+
notifyListeners(state.listeners, enriched);
|
|
136
|
+
}
|
|
137
|
+
export function emitAgentItemEvent(params) {
|
|
138
|
+
emitAgentEvent({
|
|
139
|
+
runId: params.runId,
|
|
140
|
+
stream: "item",
|
|
141
|
+
data: params.data,
|
|
142
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export function emitAgentPlanEvent(params) {
|
|
146
|
+
emitAgentEvent({
|
|
147
|
+
runId: params.runId,
|
|
148
|
+
stream: "plan",
|
|
149
|
+
data: params.data,
|
|
150
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export function emitAgentApprovalEvent(params) {
|
|
154
|
+
emitAgentEvent({
|
|
155
|
+
runId: params.runId,
|
|
156
|
+
stream: "approval",
|
|
157
|
+
data: params.data,
|
|
158
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export function emitAgentCommandOutputEvent(params) {
|
|
162
|
+
emitAgentEvent({
|
|
163
|
+
runId: params.runId,
|
|
164
|
+
stream: "command_output",
|
|
165
|
+
data: params.data,
|
|
166
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export function emitAgentPatchSummaryEvent(params) {
|
|
170
|
+
emitAgentEvent({
|
|
171
|
+
runId: params.runId,
|
|
172
|
+
stream: "patch",
|
|
173
|
+
data: params.data,
|
|
174
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function onAgentEvent(listener) {
|
|
178
|
+
const state = getAgentEventState();
|
|
179
|
+
return registerListener(state.listeners, listener);
|
|
180
|
+
}
|
|
181
|
+
export function resetAgentEventsForTest() {
|
|
182
|
+
const state = getAgentEventState();
|
|
183
|
+
state.seqByRun.clear();
|
|
184
|
+
state.listeners.clear();
|
|
185
|
+
state.runContextById.clear();
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=agent-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-events.js","sourceRoot":"","sources":["../src/agent-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAuInE,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAExE,SAAS,kBAAkB;IACzB,OAAO,sBAAsB,CAAkB,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3E,QAAQ,EAAE,IAAI,GAAG,EAAkB;QACnC,SAAS,EAAE,IAAI,GAAG,EAAoC;QACtD,cAAc,EAAE,IAAI,GAAG,EAA2B;KACnD,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa,EAAE,OAAwB;IAC7E,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,UAAU,GAAoB;YAClC,GAAG,OAAO;YACV,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC,GAAG,EAAE;SACjD,CAAC;QACF,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,QAAQ,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC;QACnF,QAAQ,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAC3C,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,QAAQ,CAAC,YAAY,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC;QACzF,QAAQ,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC/C,CAAC;IACD,IAAI,OAAO,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;QAC7C,QAAQ,CAAC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,WAAW,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC;QACtF,QAAQ,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAC7C,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,QAAQ,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC/C,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,QAAQ,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,kBAAkB,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAQ,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IAC7D,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1D,6EAA6E;QAC7E,8EAA8E;QAC9E,MAAM,QAAQ,GAAG,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,CAAC;QACtD,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;QACjD,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;YACnB,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7B,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAA4C;IACzE,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3D,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IACD,MAAM,kBAAkB,GAAG,OAAO,EAAE,kBAAkB,IAAI,IAAI,CAAC;IAC/D,MAAM,eAAe,GACnB,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IACjG,8EAA8E;IAC9E,yEAAyE;IACzE,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,kBAAkB,GAAG,kBAAkB,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC;IAC9E,MAAM,UAAU,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,eAAe,IAAI,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7F,MAAM,QAAQ,GAAsB;QAClC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,GAAG,EAAE,OAAO;QACZ,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;QACd,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpD,CAAC;IACF,eAAe,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAIlC;IACC,cAAc,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,MAAM,CAAC,IAA0C;QACvD,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAIlC;IACC,cAAc,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,MAAM,CAAC,IAA0C;QACvD,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAItC;IACC,cAAc,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,MAAM,CAAC,IAA0C;QACvD,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,MAI3C;IACC,cAAc,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,gBAAgB;QACxB,IAAI,EAAE,MAAM,CAAC,IAA0C;QACvD,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,MAI1C;IACC,cAAc,CAAC;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,MAAM,CAAC,IAA0C;QACvD,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAA0C;IACrE,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,OAAO,gBAAgB,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACvB,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACxB,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/global-singleton.ts:1-17`.
|
|
3
|
+
*
|
|
4
|
+
* Safe for process-local caches and registries that can tolerate helper-based
|
|
5
|
+
* resolution. Do not use this for live mutable state that must survive split
|
|
6
|
+
* runtime chunks; keep those on a direct `globalThis[Symbol.for(...)]` lookup.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveGlobalSingleton<T>(key: symbol, create: () => T): T;
|
|
9
|
+
export declare function resolveGlobalMap<TKey, TValue>(key: symbol): Map<TKey, TValue>;
|
|
10
|
+
//# sourceMappingURL=global-singleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-singleton.d.ts","sourceRoot":"","sources":["../src/global-singleton.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAE7E"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/global-singleton.ts:1-17`.
|
|
3
|
+
*
|
|
4
|
+
* Safe for process-local caches and registries that can tolerate helper-based
|
|
5
|
+
* resolution. Do not use this for live mutable state that must survive split
|
|
6
|
+
* runtime chunks; keep those on a direct `globalThis[Symbol.for(...)]` lookup.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveGlobalSingleton(key, create) {
|
|
9
|
+
const globalStore = globalThis;
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(globalStore, key)) {
|
|
11
|
+
return globalStore[key];
|
|
12
|
+
}
|
|
13
|
+
const created = create();
|
|
14
|
+
globalStore[key] = created;
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
export function resolveGlobalMap(key) {
|
|
18
|
+
return resolveGlobalSingleton(key, () => new Map());
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=global-singleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-singleton.js","sourceRoot":"","sources":["../src/global-singleton.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAI,GAAW,EAAE,MAAe;IACpE,MAAM,WAAW,GAAG,UAA0C,CAAC;IAC/D,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,WAAW,CAAC,GAAG,CAAM,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAe,GAAW;IACxD,OAAO,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,GAAG,EAAgB,CAAC,CAAC;AACpE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nubemclaw/agent-events — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the in-process bus + helpers + types from `agent-events.ts`.
|
|
5
|
+
* Consumers in this repo (`packages/cli`, `packages/gateway`,
|
|
6
|
+
* `packages/channel-telegram`, future `packages/gateway-client`) import
|
|
7
|
+
* from this entrypoint only — never reach inside `src/agent-events.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The two infra helpers `resolveGlobalSingleton` and
|
|
10
|
+
* `notifyListeners`/`registerListener` are intentionally NOT re-exported:
|
|
11
|
+
* they support the bus implementation, not its public contract.
|
|
12
|
+
*/
|
|
13
|
+
export type { AgentApprovalEventData, AgentApprovalEventKind, AgentApprovalEventPhase, AgentApprovalEventStatus, AgentCommandOutputEventData, AgentEventPayload, AgentEventStream, AgentItemEventData, AgentItemEventKind, AgentItemEventPhase, AgentItemEventStatus, AgentPatchSummaryEventData, AgentPlanEventData, AgentRunContext, VerboseLevel, } from "./agent-events.js";
|
|
14
|
+
export { clearAgentRunContext, emitAgentApprovalEvent, emitAgentCommandOutputEvent, emitAgentEvent, emitAgentItemEvent, emitAgentPatchSummaryEvent, emitAgentPlanEvent, getAgentRunContext, onAgentEvent, registerAgentRunContext, resetAgentEventsForTest, resetAgentRunContextForTest, sweepStaleRunContexts, } from "./agent-events.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,YAAY,EACV,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,2BAA2B,EAC3B,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,0BAA0B,EAC1B,kBAAkB,EAClB,eAAe,EACf,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,2BAA2B,EAC3B,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { clearAgentRunContext, emitAgentApprovalEvent, emitAgentCommandOutputEvent, emitAgentEvent, emitAgentItemEvent, emitAgentPatchSummaryEvent, emitAgentPlanEvent, getAgentRunContext, onAgentEvent, registerAgentRunContext, resetAgentEventsForTest, resetAgentRunContextForTest, sweepStaleRunContexts, } from "./agent-events.js";
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,2BAA2B,EAC3B,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/listeners.ts:1-22`.
|
|
3
|
+
*
|
|
4
|
+
* `notifyListeners` swallows individual listener errors via `onError?` so a
|
|
5
|
+
* misbehaving subscriber cannot break the fan-out. `registerListener` returns
|
|
6
|
+
* an unsubscribe callback in the same shape `onAgentEvent` consumers expect.
|
|
7
|
+
*/
|
|
8
|
+
export declare function notifyListeners<T>(listeners: Iterable<(event: T) => void>, event: T, onError?: (error: unknown) => void): void;
|
|
9
|
+
export declare function registerListener<T>(listeners: Set<(event: T) => void>, listener: (event: T) => void): () => void;
|
|
10
|
+
//# sourceMappingURL=listeners.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listeners.d.ts","sourceRoot":"","sources":["../src/listeners.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,SAAS,EAAE,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,EACvC,KAAK,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,SAAS,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,EAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAC3B,MAAM,IAAI,CAKZ"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/listeners.ts:1-22`.
|
|
3
|
+
*
|
|
4
|
+
* `notifyListeners` swallows individual listener errors via `onError?` so a
|
|
5
|
+
* misbehaving subscriber cannot break the fan-out. `registerListener` returns
|
|
6
|
+
* an unsubscribe callback in the same shape `onAgentEvent` consumers expect.
|
|
7
|
+
*/
|
|
8
|
+
export function notifyListeners(listeners, event, onError) {
|
|
9
|
+
for (const listener of listeners) {
|
|
10
|
+
try {
|
|
11
|
+
listener(event);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
onError?.(error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function registerListener(listeners, listener) {
|
|
19
|
+
listeners.add(listener);
|
|
20
|
+
return () => {
|
|
21
|
+
listeners.delete(listener);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=listeners.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listeners.js","sourceRoot":"","sources":["../src/listeners.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAuC,EACvC,KAAQ,EACR,OAAkC;IAElC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,QAAQ,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAkC,EAClC,QAA4B;IAE5B,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nubemclaw/agent-events",
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "NubemClaw v3 — in-process event-bus para agent lifecycle (port verbatim OpenClaw src/infra/agent-events.ts). Singleton via globalThis Symbol; cross-fase contract observable.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -b",
|
|
21
|
+
"clean": "tsc -b --clean && rm -rf dist .cache"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { AgentEventPayload } from "./agent-events.js";
|
|
4
|
+
import {
|
|
5
|
+
clearAgentRunContext,
|
|
6
|
+
emitAgentApprovalEvent,
|
|
7
|
+
emitAgentCommandOutputEvent,
|
|
8
|
+
emitAgentEvent,
|
|
9
|
+
emitAgentItemEvent,
|
|
10
|
+
emitAgentPatchSummaryEvent,
|
|
11
|
+
emitAgentPlanEvent,
|
|
12
|
+
getAgentRunContext,
|
|
13
|
+
onAgentEvent,
|
|
14
|
+
registerAgentRunContext,
|
|
15
|
+
resetAgentEventsForTest,
|
|
16
|
+
sweepStaleRunContexts,
|
|
17
|
+
} from "./agent-events.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Contract tests for the in-process bus. Mirrors the cases covered by
|
|
21
|
+
* `~/Desktop/GitHub/openclaw/src/infra/agent-events.test.ts` (where
|
|
22
|
+
* present) plus the visibility-gating semantics asserted by the
|
|
23
|
+
* dispatcher F19 wiring. The bus uses a `globalThis[Symbol.for(...)]`
|
|
24
|
+
* singleton so each test must reset state via `resetAgentEventsForTest`.
|
|
25
|
+
*/
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
resetAgentEventsForTest();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("emitAgentEvent + onAgentEvent", () => {
|
|
31
|
+
it("delivers events to all subscribers in registration order", () => {
|
|
32
|
+
const seen: AgentEventPayload[] = [];
|
|
33
|
+
onAgentEvent((evt) => {
|
|
34
|
+
seen.push(evt);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
emitAgentEvent({ runId: "run-1", stream: "lifecycle", data: { phase: "start" } });
|
|
38
|
+
|
|
39
|
+
expect(seen).toHaveLength(1);
|
|
40
|
+
expect(seen[0]?.runId).toBe("run-1");
|
|
41
|
+
expect(seen[0]?.stream).toBe("lifecycle");
|
|
42
|
+
expect(seen[0]?.seq).toBe(1);
|
|
43
|
+
expect(typeof seen[0]?.ts).toBe("number");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("increments seq per runId (and resets per run)", () => {
|
|
47
|
+
const seen: AgentEventPayload[] = [];
|
|
48
|
+
onAgentEvent((evt) => {
|
|
49
|
+
seen.push(evt);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
emitAgentEvent({ runId: "run-A", stream: "lifecycle", data: { phase: "start" } });
|
|
53
|
+
emitAgentEvent({ runId: "run-A", stream: "tool", data: { name: "search" } });
|
|
54
|
+
emitAgentEvent({ runId: "run-B", stream: "lifecycle", data: { phase: "start" } });
|
|
55
|
+
emitAgentEvent({ runId: "run-A", stream: "lifecycle", data: { phase: "end" } });
|
|
56
|
+
|
|
57
|
+
expect(seen.map((e) => `${e.runId}#${e.seq}`)).toEqual([
|
|
58
|
+
"run-A#1",
|
|
59
|
+
"run-A#2",
|
|
60
|
+
"run-B#1",
|
|
61
|
+
"run-A#3",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns an unsubscribe callback that stops further deliveries", () => {
|
|
66
|
+
const seen: AgentEventPayload[] = [];
|
|
67
|
+
const unsubscribe = onAgentEvent((evt) => {
|
|
68
|
+
seen.push(evt);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "start" } });
|
|
72
|
+
unsubscribe();
|
|
73
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "end" } });
|
|
74
|
+
|
|
75
|
+
expect(seen).toHaveLength(1);
|
|
76
|
+
expect(seen[0]?.data).toEqual({ phase: "start" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("isolates a misbehaving listener so siblings still receive the event", () => {
|
|
80
|
+
const seen: string[] = [];
|
|
81
|
+
onAgentEvent(() => {
|
|
82
|
+
throw new Error("boom");
|
|
83
|
+
});
|
|
84
|
+
onAgentEvent((evt) => {
|
|
85
|
+
seen.push(`${evt.runId}/${evt.stream}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(() =>
|
|
89
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "start" } }),
|
|
90
|
+
).not.toThrow();
|
|
91
|
+
expect(seen).toEqual(["r/lifecycle"]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("sessionKey enrichment + visibility gating", () => {
|
|
96
|
+
it("uses an event-level sessionKey verbatim when provided", () => {
|
|
97
|
+
const seen: AgentEventPayload[] = [];
|
|
98
|
+
onAgentEvent((evt) => {
|
|
99
|
+
seen.push(evt);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
emitAgentEvent({
|
|
103
|
+
runId: "r",
|
|
104
|
+
stream: "tool",
|
|
105
|
+
data: { name: "x" },
|
|
106
|
+
sessionKey: "session-explicit",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(seen[0]?.sessionKey).toBe("session-explicit");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("falls back to the registered context sessionKey", () => {
|
|
113
|
+
registerAgentRunContext("r", { sessionKey: "session-from-context" });
|
|
114
|
+
const seen: AgentEventPayload[] = [];
|
|
115
|
+
onAgentEvent((evt) => {
|
|
116
|
+
seen.push(evt);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
emitAgentEvent({ runId: "r", stream: "tool", data: { name: "x" } });
|
|
120
|
+
|
|
121
|
+
expect(seen[0]?.sessionKey).toBe("session-from-context");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("treats a blank-or-whitespace event sessionKey as absent", () => {
|
|
125
|
+
registerAgentRunContext("r", { sessionKey: "context-fallback" });
|
|
126
|
+
const seen: AgentEventPayload[] = [];
|
|
127
|
+
onAgentEvent((evt) => {
|
|
128
|
+
seen.push(evt);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
emitAgentEvent({ runId: "r", stream: "tool", data: { x: 1 }, sessionKey: " " });
|
|
132
|
+
|
|
133
|
+
expect(seen[0]?.sessionKey).toBe("context-fallback");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("redacts sessionKey on non-lifecycle streams when run is hidden", () => {
|
|
137
|
+
registerAgentRunContext("r", {
|
|
138
|
+
sessionKey: "hidden-session",
|
|
139
|
+
isControlUiVisible: false,
|
|
140
|
+
});
|
|
141
|
+
const seen: AgentEventPayload[] = [];
|
|
142
|
+
onAgentEvent((evt) => {
|
|
143
|
+
seen.push(evt);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
emitAgentEvent({ runId: "r", stream: "tool", data: { name: "x" } });
|
|
147
|
+
|
|
148
|
+
expect(seen[0]?.sessionKey).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("preserves sessionKey on the lifecycle stream even when hidden", () => {
|
|
152
|
+
registerAgentRunContext("r", {
|
|
153
|
+
sessionKey: "hidden-but-lifecycle",
|
|
154
|
+
isControlUiVisible: false,
|
|
155
|
+
});
|
|
156
|
+
const seen: AgentEventPayload[] = [];
|
|
157
|
+
onAgentEvent((evt) => {
|
|
158
|
+
seen.push(evt);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "end" } });
|
|
162
|
+
|
|
163
|
+
expect(seen[0]?.sessionKey).toBe("hidden-but-lifecycle");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("registerAgentRunContext", () => {
|
|
168
|
+
it("ignores an empty runId (no-op)", () => {
|
|
169
|
+
registerAgentRunContext("", { sessionKey: "irrelevant" });
|
|
170
|
+
expect(getAgentRunContext("")).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("creates a fresh entry with registeredAt defaulted to now", () => {
|
|
174
|
+
const before = Date.now();
|
|
175
|
+
registerAgentRunContext("r", { sessionKey: "s" });
|
|
176
|
+
const ctx = getAgentRunContext("r");
|
|
177
|
+
|
|
178
|
+
expect(ctx?.sessionKey).toBe("s");
|
|
179
|
+
expect(ctx?.registeredAt).toBeGreaterThanOrEqual(before);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("merges into an existing entry without overwriting with undefined", () => {
|
|
183
|
+
registerAgentRunContext("r", { sessionKey: "first", verboseLevel: "full" });
|
|
184
|
+
registerAgentRunContext("r", { isHeartbeat: true });
|
|
185
|
+
|
|
186
|
+
const ctx = getAgentRunContext("r");
|
|
187
|
+
expect(ctx?.sessionKey).toBe("first");
|
|
188
|
+
expect(ctx?.verboseLevel).toBe("full");
|
|
189
|
+
expect(ctx?.isHeartbeat).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("allows toggling isControlUiVisible to false explicitly", () => {
|
|
193
|
+
registerAgentRunContext("r", { isControlUiVisible: true });
|
|
194
|
+
registerAgentRunContext("r", { isControlUiVisible: false });
|
|
195
|
+
|
|
196
|
+
expect(getAgentRunContext("r")?.isControlUiVisible).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("clearAgentRunContext", () => {
|
|
201
|
+
it("removes the context entry and its seq counter", () => {
|
|
202
|
+
registerAgentRunContext("r", { sessionKey: "s" });
|
|
203
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "start" } });
|
|
204
|
+
emitAgentEvent({ runId: "r", stream: "tool", data: { name: "x" } });
|
|
205
|
+
|
|
206
|
+
clearAgentRunContext("r");
|
|
207
|
+
expect(getAgentRunContext("r")).toBeUndefined();
|
|
208
|
+
|
|
209
|
+
const seen: AgentEventPayload[] = [];
|
|
210
|
+
onAgentEvent((evt) => {
|
|
211
|
+
seen.push(evt);
|
|
212
|
+
});
|
|
213
|
+
emitAgentEvent({ runId: "r", stream: "lifecycle", data: { phase: "restart" } });
|
|
214
|
+
|
|
215
|
+
expect(seen[0]?.seq).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("sweepStaleRunContexts", () => {
|
|
220
|
+
it("removes entries whose lastActiveAt exceeds the TTL", () => {
|
|
221
|
+
registerAgentRunContext("old", {
|
|
222
|
+
registeredAt: Date.now() - 60_000,
|
|
223
|
+
lastActiveAt: Date.now() - 60_000,
|
|
224
|
+
});
|
|
225
|
+
registerAgentRunContext("fresh", { lastActiveAt: Date.now() });
|
|
226
|
+
|
|
227
|
+
const swept = sweepStaleRunContexts(30_000);
|
|
228
|
+
|
|
229
|
+
expect(swept).toBe(1);
|
|
230
|
+
expect(getAgentRunContext("old")).toBeUndefined();
|
|
231
|
+
expect(getAgentRunContext("fresh")).toBeDefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses registeredAt when lastActiveAt is missing", () => {
|
|
235
|
+
registerAgentRunContext("partial", { registeredAt: Date.now() - 60_000 });
|
|
236
|
+
|
|
237
|
+
const swept = sweepStaleRunContexts(30_000);
|
|
238
|
+
|
|
239
|
+
expect(swept).toBe(1);
|
|
240
|
+
expect(getAgentRunContext("partial")).toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("treats entries with no timestamp as infinitely old", () => {
|
|
244
|
+
registerAgentRunContext("ghost", { sessionKey: "abc" });
|
|
245
|
+
const ctx = getAgentRunContext("ghost");
|
|
246
|
+
if (ctx) {
|
|
247
|
+
delete ctx.registeredAt;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const swept = sweepStaleRunContexts(30_000);
|
|
251
|
+
|
|
252
|
+
expect(swept).toBe(1);
|
|
253
|
+
expect(getAgentRunContext("ghost")).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("typed helpers", () => {
|
|
258
|
+
it("emitAgentItemEvent emits with stream='item' and forwards sessionKey", () => {
|
|
259
|
+
const seen: AgentEventPayload[] = [];
|
|
260
|
+
onAgentEvent((evt) => {
|
|
261
|
+
seen.push(evt);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
emitAgentItemEvent({
|
|
265
|
+
runId: "r",
|
|
266
|
+
sessionKey: "sess",
|
|
267
|
+
data: {
|
|
268
|
+
itemId: "item-1",
|
|
269
|
+
phase: "start",
|
|
270
|
+
kind: "tool",
|
|
271
|
+
title: "Run shell",
|
|
272
|
+
status: "running",
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(seen[0]?.stream).toBe("item");
|
|
277
|
+
expect(seen[0]?.sessionKey).toBe("sess");
|
|
278
|
+
expect(seen[0]?.data).toMatchObject({ itemId: "item-1", phase: "start" });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("emitAgentPlanEvent emits with stream='plan'", () => {
|
|
282
|
+
const seen: AgentEventPayload[] = [];
|
|
283
|
+
onAgentEvent((evt) => {
|
|
284
|
+
seen.push(evt);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
emitAgentPlanEvent({
|
|
288
|
+
runId: "r",
|
|
289
|
+
data: { phase: "update", title: "Plan", steps: ["a", "b"] },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(seen[0]?.stream).toBe("plan");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("emitAgentApprovalEvent emits with stream='approval'", () => {
|
|
296
|
+
const seen: AgentEventPayload[] = [];
|
|
297
|
+
onAgentEvent((evt) => {
|
|
298
|
+
seen.push(evt);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
emitAgentApprovalEvent({
|
|
302
|
+
runId: "r",
|
|
303
|
+
data: {
|
|
304
|
+
phase: "requested",
|
|
305
|
+
kind: "exec",
|
|
306
|
+
status: "pending",
|
|
307
|
+
title: "Approve curl",
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(seen[0]?.stream).toBe("approval");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("emitAgentCommandOutputEvent emits with stream='command_output'", () => {
|
|
315
|
+
const seen: AgentEventPayload[] = [];
|
|
316
|
+
onAgentEvent((evt) => {
|
|
317
|
+
seen.push(evt);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
emitAgentCommandOutputEvent({
|
|
321
|
+
runId: "r",
|
|
322
|
+
data: {
|
|
323
|
+
itemId: "i",
|
|
324
|
+
phase: "delta",
|
|
325
|
+
title: "shell",
|
|
326
|
+
toolCallId: "tc-1",
|
|
327
|
+
output: "hello\n",
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(seen[0]?.stream).toBe("command_output");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("emitAgentPatchSummaryEvent emits with stream='patch'", () => {
|
|
335
|
+
const seen: AgentEventPayload[] = [];
|
|
336
|
+
onAgentEvent((evt) => {
|
|
337
|
+
seen.push(evt);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
emitAgentPatchSummaryEvent({
|
|
341
|
+
runId: "r",
|
|
342
|
+
data: {
|
|
343
|
+
itemId: "i",
|
|
344
|
+
phase: "end",
|
|
345
|
+
title: "Apply patch",
|
|
346
|
+
toolCallId: "tc-1",
|
|
347
|
+
added: ["a.ts"],
|
|
348
|
+
modified: [],
|
|
349
|
+
deleted: [],
|
|
350
|
+
summary: "+1/-0",
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(seen[0]?.stream).toBe("patch");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-events — port verbatim de
|
|
3
|
+
* `~/Desktop/GitHub/openclaw/src/infra/agent-events.ts:1-312`.
|
|
4
|
+
*
|
|
5
|
+
* In-process event bus para agent lifecycle/tool/assistant/etc. La
|
|
6
|
+
* elección del singleton mediante `globalThis[Symbol.for(...)]` (no
|
|
7
|
+
* un módulo-state local) sobrevive a chunks de runtime split (ESM
|
|
8
|
+
* bundles, vitest worker pools, sub-process forks que se re-importan
|
|
9
|
+
* el módulo). El contrato observable es el corazón de cómo TUI
|
|
10
|
+
* embedded, gateway HTTP/WS y daemon se "atan" sin acoplarse — todos
|
|
11
|
+
* los runners suben el mismo bus, todos los listeners ven el mismo
|
|
12
|
+
* stream.
|
|
13
|
+
*
|
|
14
|
+
* Cambios mínimos frente al verbatim OpenClaw:
|
|
15
|
+
*
|
|
16
|
+
* 1. `Symbol.for("openclaw.agentEvents.state")` →
|
|
17
|
+
* `Symbol.for("nubemclaw.agentEvents.state")` por la regla de
|
|
18
|
+
* rebrand canónico ADR-0003 (la palabra "openclaw" no aparece
|
|
19
|
+
* en código v3 fuera de comentarios de ingeniería inversa).
|
|
20
|
+
* 2. `VerboseLevel` inline (declarado aquí, no importado de un
|
|
21
|
+
* módulo `auto-reply/thinking.shared.ts` que v3 no tiene).
|
|
22
|
+
* Tomado verbatim de OpenClaw thinking.shared.ts:18.
|
|
23
|
+
* 3. `emitAgentEvent` construye `enriched` con spread condicional
|
|
24
|
+
* cuando `sessionKey === undefined`, porque v3 usa
|
|
25
|
+
* `exactOptionalPropertyTypes: true` y no permite asignar
|
|
26
|
+
* `undefined` a un campo `?: string`. Shape semantics idénticas.
|
|
27
|
+
* 4. Imports relativos con extensión `.js` (NodeNext ESM v3).
|
|
28
|
+
*
|
|
29
|
+
* El resto (símbolos, line shape, comentarios sustanciales,
|
|
30
|
+
* orden de funciones) se preserva 1:1.
|
|
31
|
+
*/
|
|
32
|
+
import { resolveGlobalSingleton } from "./global-singleton.js";
|
|
33
|
+
import { notifyListeners, registerListener } from "./listeners.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verbose level (port verbatim OpenClaw `auto-reply/thinking.shared.ts:18`).
|
|
37
|
+
*
|
|
38
|
+
* `off` silences agent commentary; `on` emits standard logs;
|
|
39
|
+
* `full` includes diagnostic detail. Surfaced here so run contexts
|
|
40
|
+
* carry the level without coupling agent-events to a thinking module.
|
|
41
|
+
*/
|
|
42
|
+
export type VerboseLevel = "off" | "on" | "full";
|
|
43
|
+
|
|
44
|
+
export type AgentEventStream =
|
|
45
|
+
| "lifecycle"
|
|
46
|
+
| "tool"
|
|
47
|
+
| "assistant"
|
|
48
|
+
| "error"
|
|
49
|
+
| "item"
|
|
50
|
+
| "plan"
|
|
51
|
+
| "approval"
|
|
52
|
+
| "command_output"
|
|
53
|
+
| "patch"
|
|
54
|
+
| "compaction"
|
|
55
|
+
| "thinking"
|
|
56
|
+
| (string & {});
|
|
57
|
+
|
|
58
|
+
export type AgentItemEventPhase = "start" | "update" | "end";
|
|
59
|
+
export type AgentItemEventStatus = "running" | "completed" | "failed" | "blocked";
|
|
60
|
+
export type AgentItemEventKind =
|
|
61
|
+
| "tool"
|
|
62
|
+
| "command"
|
|
63
|
+
| "patch"
|
|
64
|
+
| "search"
|
|
65
|
+
| "analysis"
|
|
66
|
+
| (string & {});
|
|
67
|
+
|
|
68
|
+
export type AgentItemEventData = {
|
|
69
|
+
itemId: string;
|
|
70
|
+
phase: AgentItemEventPhase;
|
|
71
|
+
kind: AgentItemEventKind;
|
|
72
|
+
title: string;
|
|
73
|
+
status: AgentItemEventStatus;
|
|
74
|
+
name?: string;
|
|
75
|
+
meta?: string;
|
|
76
|
+
toolCallId?: string;
|
|
77
|
+
startedAt?: number;
|
|
78
|
+
endedAt?: number;
|
|
79
|
+
error?: string;
|
|
80
|
+
summary?: string;
|
|
81
|
+
progressText?: string;
|
|
82
|
+
/** Preserve item telemetry while letting channel progress render a sibling tool event instead. */
|
|
83
|
+
suppressChannelProgress?: boolean;
|
|
84
|
+
approvalId?: string;
|
|
85
|
+
approvalSlug?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type AgentPlanEventData = {
|
|
89
|
+
phase: "update";
|
|
90
|
+
title: string;
|
|
91
|
+
explanation?: string;
|
|
92
|
+
steps?: string[];
|
|
93
|
+
source?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type AgentApprovalEventPhase = "requested" | "resolved";
|
|
97
|
+
export type AgentApprovalEventStatus = "pending" | "unavailable" | "approved" | "denied" | "failed";
|
|
98
|
+
export type AgentApprovalEventKind = "exec" | "plugin" | "unknown";
|
|
99
|
+
|
|
100
|
+
export type AgentApprovalEventData = {
|
|
101
|
+
phase: AgentApprovalEventPhase;
|
|
102
|
+
kind: AgentApprovalEventKind;
|
|
103
|
+
status: AgentApprovalEventStatus;
|
|
104
|
+
title: string;
|
|
105
|
+
itemId?: string;
|
|
106
|
+
toolCallId?: string;
|
|
107
|
+
approvalId?: string;
|
|
108
|
+
approvalSlug?: string;
|
|
109
|
+
command?: string;
|
|
110
|
+
host?: string;
|
|
111
|
+
reason?: string;
|
|
112
|
+
scope?: "turn" | "session";
|
|
113
|
+
message?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type AgentCommandOutputEventData = {
|
|
117
|
+
itemId: string;
|
|
118
|
+
phase: "delta" | "end";
|
|
119
|
+
title: string;
|
|
120
|
+
toolCallId: string;
|
|
121
|
+
name?: string;
|
|
122
|
+
output?: string;
|
|
123
|
+
status?: AgentItemEventStatus | "running";
|
|
124
|
+
exitCode?: number | null;
|
|
125
|
+
durationMs?: number;
|
|
126
|
+
cwd?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type AgentPatchSummaryEventData = {
|
|
130
|
+
itemId: string;
|
|
131
|
+
phase: "end";
|
|
132
|
+
title: string;
|
|
133
|
+
toolCallId: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
added: string[];
|
|
136
|
+
modified: string[];
|
|
137
|
+
deleted: string[];
|
|
138
|
+
summary: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type AgentEventPayload = {
|
|
142
|
+
runId: string;
|
|
143
|
+
seq: number;
|
|
144
|
+
stream: AgentEventStream;
|
|
145
|
+
ts: number;
|
|
146
|
+
data: Record<string, unknown>;
|
|
147
|
+
sessionKey?: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type AgentRunContext = {
|
|
151
|
+
sessionKey?: string;
|
|
152
|
+
verboseLevel?: VerboseLevel;
|
|
153
|
+
isHeartbeat?: boolean;
|
|
154
|
+
/** Whether control UI clients should receive chat/agent updates for this run. */
|
|
155
|
+
isControlUiVisible?: boolean;
|
|
156
|
+
/** Timestamp when this context was first registered (for TTL-based cleanup). */
|
|
157
|
+
registeredAt?: number;
|
|
158
|
+
/** Timestamp of last activity (updated on every emitAgentEvent). */
|
|
159
|
+
lastActiveAt?: number;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type AgentEventState = {
|
|
163
|
+
seqByRun: Map<string, number>;
|
|
164
|
+
listeners: Set<(evt: AgentEventPayload) => void>;
|
|
165
|
+
runContextById: Map<string, AgentRunContext>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const AGENT_EVENT_STATE_KEY = Symbol.for("nubemclaw.agentEvents.state");
|
|
169
|
+
|
|
170
|
+
function getAgentEventState(): AgentEventState {
|
|
171
|
+
return resolveGlobalSingleton<AgentEventState>(AGENT_EVENT_STATE_KEY, () => ({
|
|
172
|
+
seqByRun: new Map<string, number>(),
|
|
173
|
+
listeners: new Set<(evt: AgentEventPayload) => void>(),
|
|
174
|
+
runContextById: new Map<string, AgentRunContext>(),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function registerAgentRunContext(runId: string, context: AgentRunContext): void {
|
|
179
|
+
if (!runId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const state = getAgentEventState();
|
|
183
|
+
const existing = state.runContextById.get(runId);
|
|
184
|
+
if (!existing) {
|
|
185
|
+
const registered: AgentRunContext = {
|
|
186
|
+
...context,
|
|
187
|
+
registeredAt: context.registeredAt ?? Date.now(),
|
|
188
|
+
};
|
|
189
|
+
state.runContextById.set(runId, registered);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (context.sessionKey !== undefined && existing.sessionKey !== context.sessionKey) {
|
|
193
|
+
existing.sessionKey = context.sessionKey;
|
|
194
|
+
}
|
|
195
|
+
if (context.verboseLevel !== undefined && existing.verboseLevel !== context.verboseLevel) {
|
|
196
|
+
existing.verboseLevel = context.verboseLevel;
|
|
197
|
+
}
|
|
198
|
+
if (context.isControlUiVisible !== undefined) {
|
|
199
|
+
existing.isControlUiVisible = context.isControlUiVisible;
|
|
200
|
+
}
|
|
201
|
+
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
|
202
|
+
existing.isHeartbeat = context.isHeartbeat;
|
|
203
|
+
}
|
|
204
|
+
if (context.registeredAt !== undefined) {
|
|
205
|
+
existing.registeredAt = context.registeredAt;
|
|
206
|
+
}
|
|
207
|
+
if (context.lastActiveAt !== undefined) {
|
|
208
|
+
existing.lastActiveAt = context.lastActiveAt;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getAgentRunContext(runId: string): AgentRunContext | undefined {
|
|
213
|
+
return getAgentEventState().runContextById.get(runId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function clearAgentRunContext(runId: string): void {
|
|
217
|
+
const state = getAgentEventState();
|
|
218
|
+
state.runContextById.delete(runId);
|
|
219
|
+
state.seqByRun.delete(runId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Sweep stale run contexts that exceeded the given TTL.
|
|
224
|
+
* Guards against orphaned entries when lifecycle "end"/"error" events are missed.
|
|
225
|
+
*/
|
|
226
|
+
export function sweepStaleRunContexts(maxAgeMs = 30 * 60 * 1000): number {
|
|
227
|
+
const state = getAgentEventState();
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
let swept = 0;
|
|
230
|
+
for (const [runId, ctx] of state.runContextById.entries()) {
|
|
231
|
+
// Use lastActiveAt (refreshed on every event) to avoid sweeping active runs.
|
|
232
|
+
// Fall back to registeredAt, then treat missing timestamps as infinitely old.
|
|
233
|
+
const lastSeen = ctx.lastActiveAt ?? ctx.registeredAt;
|
|
234
|
+
const age = lastSeen ? now - lastSeen : Infinity;
|
|
235
|
+
if (age > maxAgeMs) {
|
|
236
|
+
state.runContextById.delete(runId);
|
|
237
|
+
state.seqByRun.delete(runId);
|
|
238
|
+
swept++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return swept;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function resetAgentRunContextForTest(): void {
|
|
245
|
+
const state = getAgentEventState();
|
|
246
|
+
state.runContextById.clear();
|
|
247
|
+
state.seqByRun.clear();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">): void {
|
|
251
|
+
const state = getAgentEventState();
|
|
252
|
+
const nextSeq = (state.seqByRun.get(event.runId) ?? 0) + 1;
|
|
253
|
+
state.seqByRun.set(event.runId, nextSeq);
|
|
254
|
+
const context = state.runContextById.get(event.runId);
|
|
255
|
+
if (context) {
|
|
256
|
+
context.lastActiveAt = Date.now();
|
|
257
|
+
}
|
|
258
|
+
const isControlUiVisible = context?.isControlUiVisible ?? true;
|
|
259
|
+
const eventSessionKey =
|
|
260
|
+
typeof event.sessionKey === "string" && event.sessionKey.trim() ? event.sessionKey : undefined;
|
|
261
|
+
// Hidden channel-routed runs should not leak live assistant/tool traffic into
|
|
262
|
+
// Control UI, but lifecycle events still need the session key so gateway
|
|
263
|
+
// listeners can persist terminal session state even if run-context lookup is
|
|
264
|
+
// unavailable by the time the terminal event arrives. Terminal failures are
|
|
265
|
+
// emitted on the lifecycle stream with `phase: "error"`; the separate error
|
|
266
|
+
// stream remains redacted for hidden runs because it is observational only.
|
|
267
|
+
const preserveSessionKey = isControlUiVisible || event.stream === "lifecycle";
|
|
268
|
+
const sessionKey = preserveSessionKey ? (eventSessionKey ?? context?.sessionKey) : undefined;
|
|
269
|
+
const enriched: AgentEventPayload = {
|
|
270
|
+
runId: event.runId,
|
|
271
|
+
stream: event.stream,
|
|
272
|
+
data: event.data,
|
|
273
|
+
seq: nextSeq,
|
|
274
|
+
ts: Date.now(),
|
|
275
|
+
...(sessionKey !== undefined ? { sessionKey } : {}),
|
|
276
|
+
};
|
|
277
|
+
notifyListeners(state.listeners, enriched);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function emitAgentItemEvent(params: {
|
|
281
|
+
runId: string;
|
|
282
|
+
data: AgentItemEventData;
|
|
283
|
+
sessionKey?: string;
|
|
284
|
+
}): void {
|
|
285
|
+
emitAgentEvent({
|
|
286
|
+
runId: params.runId,
|
|
287
|
+
stream: "item",
|
|
288
|
+
data: params.data as unknown as Record<string, unknown>,
|
|
289
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function emitAgentPlanEvent(params: {
|
|
294
|
+
runId: string;
|
|
295
|
+
data: AgentPlanEventData;
|
|
296
|
+
sessionKey?: string;
|
|
297
|
+
}): void {
|
|
298
|
+
emitAgentEvent({
|
|
299
|
+
runId: params.runId,
|
|
300
|
+
stream: "plan",
|
|
301
|
+
data: params.data as unknown as Record<string, unknown>,
|
|
302
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function emitAgentApprovalEvent(params: {
|
|
307
|
+
runId: string;
|
|
308
|
+
data: AgentApprovalEventData;
|
|
309
|
+
sessionKey?: string;
|
|
310
|
+
}): void {
|
|
311
|
+
emitAgentEvent({
|
|
312
|
+
runId: params.runId,
|
|
313
|
+
stream: "approval",
|
|
314
|
+
data: params.data as unknown as Record<string, unknown>,
|
|
315
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function emitAgentCommandOutputEvent(params: {
|
|
320
|
+
runId: string;
|
|
321
|
+
data: AgentCommandOutputEventData;
|
|
322
|
+
sessionKey?: string;
|
|
323
|
+
}): void {
|
|
324
|
+
emitAgentEvent({
|
|
325
|
+
runId: params.runId,
|
|
326
|
+
stream: "command_output",
|
|
327
|
+
data: params.data as unknown as Record<string, unknown>,
|
|
328
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function emitAgentPatchSummaryEvent(params: {
|
|
333
|
+
runId: string;
|
|
334
|
+
data: AgentPatchSummaryEventData;
|
|
335
|
+
sessionKey?: string;
|
|
336
|
+
}): void {
|
|
337
|
+
emitAgentEvent({
|
|
338
|
+
runId: params.runId,
|
|
339
|
+
stream: "patch",
|
|
340
|
+
data: params.data as unknown as Record<string, unknown>,
|
|
341
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void {
|
|
346
|
+
const state = getAgentEventState();
|
|
347
|
+
return registerListener(state.listeners, listener);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function resetAgentEventsForTest(): void {
|
|
351
|
+
const state = getAgentEventState();
|
|
352
|
+
state.seqByRun.clear();
|
|
353
|
+
state.listeners.clear();
|
|
354
|
+
state.runContextById.clear();
|
|
355
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/global-singleton.ts:1-17`.
|
|
3
|
+
*
|
|
4
|
+
* Safe for process-local caches and registries that can tolerate helper-based
|
|
5
|
+
* resolution. Do not use this for live mutable state that must survive split
|
|
6
|
+
* runtime chunks; keep those on a direct `globalThis[Symbol.for(...)]` lookup.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveGlobalSingleton<T>(key: symbol, create: () => T): T {
|
|
9
|
+
const globalStore = globalThis as Record<PropertyKey, unknown>;
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(globalStore, key)) {
|
|
11
|
+
return globalStore[key] as T;
|
|
12
|
+
}
|
|
13
|
+
const created = create();
|
|
14
|
+
globalStore[key] = created;
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveGlobalMap<TKey, TValue>(key: symbol): Map<TKey, TValue> {
|
|
19
|
+
return resolveGlobalSingleton(key, () => new Map<TKey, TValue>());
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nubemclaw/agent-events — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the in-process bus + helpers + types from `agent-events.ts`.
|
|
5
|
+
* Consumers in this repo (`packages/cli`, `packages/gateway`,
|
|
6
|
+
* `packages/channel-telegram`, future `packages/gateway-client`) import
|
|
7
|
+
* from this entrypoint only — never reach inside `src/agent-events.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The two infra helpers `resolveGlobalSingleton` and
|
|
10
|
+
* `notifyListeners`/`registerListener` are intentionally NOT re-exported:
|
|
11
|
+
* they support the bus implementation, not its public contract.
|
|
12
|
+
*/
|
|
13
|
+
export type {
|
|
14
|
+
AgentApprovalEventData,
|
|
15
|
+
AgentApprovalEventKind,
|
|
16
|
+
AgentApprovalEventPhase,
|
|
17
|
+
AgentApprovalEventStatus,
|
|
18
|
+
AgentCommandOutputEventData,
|
|
19
|
+
AgentEventPayload,
|
|
20
|
+
AgentEventStream,
|
|
21
|
+
AgentItemEventData,
|
|
22
|
+
AgentItemEventKind,
|
|
23
|
+
AgentItemEventPhase,
|
|
24
|
+
AgentItemEventStatus,
|
|
25
|
+
AgentPatchSummaryEventData,
|
|
26
|
+
AgentPlanEventData,
|
|
27
|
+
AgentRunContext,
|
|
28
|
+
VerboseLevel,
|
|
29
|
+
} from "./agent-events.js";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
clearAgentRunContext,
|
|
33
|
+
emitAgentApprovalEvent,
|
|
34
|
+
emitAgentCommandOutputEvent,
|
|
35
|
+
emitAgentEvent,
|
|
36
|
+
emitAgentItemEvent,
|
|
37
|
+
emitAgentPatchSummaryEvent,
|
|
38
|
+
emitAgentPlanEvent,
|
|
39
|
+
getAgentRunContext,
|
|
40
|
+
onAgentEvent,
|
|
41
|
+
registerAgentRunContext,
|
|
42
|
+
resetAgentEventsForTest,
|
|
43
|
+
resetAgentRunContextForTest,
|
|
44
|
+
sweepStaleRunContexts,
|
|
45
|
+
} from "./agent-events.js";
|
package/src/listeners.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port verbatim de `~/Desktop/GitHub/openclaw/src/shared/listeners.ts:1-22`.
|
|
3
|
+
*
|
|
4
|
+
* `notifyListeners` swallows individual listener errors via `onError?` so a
|
|
5
|
+
* misbehaving subscriber cannot break the fan-out. `registerListener` returns
|
|
6
|
+
* an unsubscribe callback in the same shape `onAgentEvent` consumers expect.
|
|
7
|
+
*/
|
|
8
|
+
export function notifyListeners<T>(
|
|
9
|
+
listeners: Iterable<(event: T) => void>,
|
|
10
|
+
event: T,
|
|
11
|
+
onError?: (error: unknown) => void,
|
|
12
|
+
): void {
|
|
13
|
+
for (const listener of listeners) {
|
|
14
|
+
try {
|
|
15
|
+
listener(event);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
onError?.(error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerListener<T>(
|
|
23
|
+
listeners: Set<(event: T) => void>,
|
|
24
|
+
listener: (event: T) => void,
|
|
25
|
+
): () => void {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
}
|