@lumenflow/surfaces 5.1.0
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/LICENSE +190 -0
- package/README.md +40 -0
- package/cli/__tests__/gates.test.ts +97 -0
- package/cli/__tests__/inspect.test.ts +184 -0
- package/cli/__tests__/task-lifecycle.test.ts +203 -0
- package/cli/gates.ts +46 -0
- package/cli/index.ts +6 -0
- package/cli/inspect.ts +138 -0
- package/cli/task-lifecycle.ts +46 -0
- package/http/__tests__/agent-runtime-remote-controls.test.ts +249 -0
- package/http/__tests__/auth-boundary.test.ts +57 -0
- package/http/__tests__/channel-send-governance.test.ts +158 -0
- package/http/__tests__/event-stream.test.ts +340 -0
- package/http/__tests__/phone-device-tool-api.test.ts +177 -0
- package/http/__tests__/remote-exposure.test.ts +212 -0
- package/http/__tests__/run-agent.test.ts +447 -0
- package/http/__tests__/scope-enforcement.test.ts +349 -0
- package/http/__tests__/sidecar-entry.test.ts +158 -0
- package/http/__tests__/tool-api-schema-validation.test.ts +213 -0
- package/http/__tests__/tool-api.test.ts +491 -0
- package/http/__tests__/tool-discovery.test.ts +384 -0
- package/http/ag-ui-adapter.ts +352 -0
- package/http/auth.ts +294 -0
- package/http/control-plane-event-subscriber.ts +233 -0
- package/http/event-stream.ts +216 -0
- package/http/index.ts +10 -0
- package/http/run-agent.ts +416 -0
- package/http/server.ts +329 -0
- package/http/sidecar-entry.ts +218 -0
- package/http/task-api.ts +307 -0
- package/http/tool-api.ts +373 -0
- package/http/tool-discovery.ts +159 -0
- package/mcp/__tests__/server.test.ts +554 -0
- package/mcp/index.ts +4 -0
- package/mcp/server.ts +250 -0
- package/package.json +51 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { Disposable, KernelEvent, ReplayFilter } from '@lumenflow/kernel';
|
|
5
|
+
import type { EventSubscriber } from './event-stream.js';
|
|
6
|
+
|
|
7
|
+
const CONTROL_PLANE_POLICY_DECISION = {
|
|
8
|
+
DENY: 'deny',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
const DIAGNOSTIC_REASON = {
|
|
12
|
+
POLICY_PULL_FAILED: 'policy pull failed',
|
|
13
|
+
HEARTBEAT_FAILED: 'heartbeat failed',
|
|
14
|
+
EVENT_PUSH_FAILED: 'event forwarding failed',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const ACTIONABLE_SYNC_DIAGNOSTIC_SUFFIX = 'Check control_plane.endpoint and auth token.';
|
|
18
|
+
const WORKSPACE_WARNING_EVENT_KIND = 'workspace_warning';
|
|
19
|
+
const KERNEL_EVENT_SCHEMA_VERSION = 1;
|
|
20
|
+
const DEFAULT_SYNC_INTERVAL_MS = 30_000;
|
|
21
|
+
const SYNC_SESSION_ID_PREFIX = 'http-surface-sync';
|
|
22
|
+
const CLOCK_ISO_OFFSET = 36;
|
|
23
|
+
|
|
24
|
+
interface ControlPlanePolicyRuleLike {
|
|
25
|
+
id: string;
|
|
26
|
+
decision: 'allow' | 'deny';
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ControlPlanePolicySetLike {
|
|
31
|
+
default_decision: 'allow' | 'deny';
|
|
32
|
+
rules: ControlPlanePolicyRuleLike[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ControlPlaneSyncPortLike {
|
|
36
|
+
pullPolicies(input: { workspace_id: string }): Promise<ControlPlanePolicySetLike>;
|
|
37
|
+
heartbeat(input: {
|
|
38
|
+
workspace_id: string;
|
|
39
|
+
session_id: string;
|
|
40
|
+
}): Promise<{ status: 'ok'; server_time: string }>;
|
|
41
|
+
pushKernelEvents(input: {
|
|
42
|
+
workspace_id: string;
|
|
43
|
+
events: KernelEvent[];
|
|
44
|
+
}): Promise<{ accepted: number }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ControlPlaneEventSubscriberOptions {
|
|
48
|
+
controlPlaneSyncPort: ControlPlaneSyncPortLike;
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
pollIntervalMs: number;
|
|
51
|
+
logger?: Pick<Console, 'warn'>;
|
|
52
|
+
now?: () => Date;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_CONTROL_PLANE_SYNC_INTERVAL_MS = DEFAULT_SYNC_INTERVAL_MS;
|
|
56
|
+
|
|
57
|
+
interface SyncState {
|
|
58
|
+
allowForwarding: boolean;
|
|
59
|
+
inFlight: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createWorkspaceWarningEvent(message: string, now: () => Date): KernelEvent {
|
|
63
|
+
return {
|
|
64
|
+
schema_version: KERNEL_EVENT_SCHEMA_VERSION,
|
|
65
|
+
kind: WORKSPACE_WARNING_EVENT_KIND,
|
|
66
|
+
timestamp: now().toISOString(),
|
|
67
|
+
message,
|
|
68
|
+
} as KernelEvent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createSyncSessionId(now: () => Date): string {
|
|
72
|
+
return `${SYNC_SESSION_ID_PREFIX}:${now().toISOString().slice(0, CLOCK_ISO_OFFSET)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function emitDiagnostic(
|
|
76
|
+
callback: (event: KernelEvent) => void | Promise<void>,
|
|
77
|
+
logger: Pick<Console, 'warn'> | undefined,
|
|
78
|
+
workspaceId: string,
|
|
79
|
+
reason: string,
|
|
80
|
+
now: () => Date,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const message =
|
|
83
|
+
`Control plane sync ${reason} for workspace "${workspaceId}". ` +
|
|
84
|
+
ACTIONABLE_SYNC_DIAGNOSTIC_SUFFIX;
|
|
85
|
+
logger?.warn?.(message);
|
|
86
|
+
await callback(createWorkspaceWarningEvent(message, now));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runSyncCycle(
|
|
90
|
+
options: ControlPlaneEventSubscriberOptions,
|
|
91
|
+
callback: (event: KernelEvent) => void | Promise<void>,
|
|
92
|
+
sessionId: string,
|
|
93
|
+
pendingEvents: KernelEvent[],
|
|
94
|
+
state: SyncState,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (state.inFlight) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
state.inFlight = true;
|
|
100
|
+
const now = options.now ?? (() => new Date());
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
try {
|
|
104
|
+
const pulledPolicies = await options.controlPlaneSyncPort.pullPolicies({
|
|
105
|
+
workspace_id: options.workspaceId,
|
|
106
|
+
});
|
|
107
|
+
state.allowForwarding =
|
|
108
|
+
pulledPolicies.default_decision !== CONTROL_PLANE_POLICY_DECISION.DENY;
|
|
109
|
+
} catch {
|
|
110
|
+
state.allowForwarding = false;
|
|
111
|
+
await emitDiagnostic(
|
|
112
|
+
callback,
|
|
113
|
+
options.logger,
|
|
114
|
+
options.workspaceId,
|
|
115
|
+
DIAGNOSTIC_REASON.POLICY_PULL_FAILED,
|
|
116
|
+
now,
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await options.controlPlaneSyncPort.heartbeat({
|
|
123
|
+
workspace_id: options.workspaceId,
|
|
124
|
+
session_id: sessionId,
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
await emitDiagnostic(
|
|
128
|
+
callback,
|
|
129
|
+
options.logger,
|
|
130
|
+
options.workspaceId,
|
|
131
|
+
DIAGNOSTIC_REASON.HEARTBEAT_FAILED,
|
|
132
|
+
now,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!state.allowForwarding || pendingEvents.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const batch = pendingEvents.splice(0, pendingEvents.length);
|
|
142
|
+
try {
|
|
143
|
+
await options.controlPlaneSyncPort.pushKernelEvents({
|
|
144
|
+
workspace_id: options.workspaceId,
|
|
145
|
+
events: batch,
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
pendingEvents.unshift(...batch);
|
|
149
|
+
state.allowForwarding = false;
|
|
150
|
+
await emitDiagnostic(
|
|
151
|
+
callback,
|
|
152
|
+
options.logger,
|
|
153
|
+
options.workspaceId,
|
|
154
|
+
DIAGNOSTIC_REASON.EVENT_PUSH_FAILED,
|
|
155
|
+
now,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
state.inFlight = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function wrapEventSubscriberWithControlPlaneSync(
|
|
164
|
+
source: EventSubscriber,
|
|
165
|
+
options: ControlPlaneEventSubscriberOptions,
|
|
166
|
+
): EventSubscriber {
|
|
167
|
+
const resolvedIntervalMs =
|
|
168
|
+
options.pollIntervalMs > 0 ? options.pollIntervalMs : DEFAULT_CONTROL_PLANE_SYNC_INTERVAL_MS;
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
subscribe(
|
|
172
|
+
filter: ReplayFilter,
|
|
173
|
+
callback: (event: KernelEvent) => void | Promise<void>,
|
|
174
|
+
): Disposable {
|
|
175
|
+
const pendingEvents: KernelEvent[] = [];
|
|
176
|
+
const state: SyncState = {
|
|
177
|
+
allowForwarding: true,
|
|
178
|
+
inFlight: false,
|
|
179
|
+
};
|
|
180
|
+
const now = options.now ?? (() => new Date());
|
|
181
|
+
const sessionId = createSyncSessionId(now);
|
|
182
|
+
|
|
183
|
+
const wrappedCallback = async (event: KernelEvent): Promise<void> => {
|
|
184
|
+
await callback(event);
|
|
185
|
+
pendingEvents.push(event);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const sourceSubscription = source.subscribe(filter, wrappedCallback);
|
|
189
|
+
const timer = setInterval(() => {
|
|
190
|
+
void runSyncCycle(options, callback, sessionId, pendingEvents, state);
|
|
191
|
+
}, resolvedIntervalMs);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
dispose: () => {
|
|
195
|
+
clearInterval(timer);
|
|
196
|
+
sourceSubscription.dispose();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Creates an EventSubscriber that can be used by the dashboard to
|
|
205
|
+
* optionally read events from the control plane instead of a local
|
|
206
|
+
* EventStore. This enables centralized observability for organizations
|
|
207
|
+
* where the web dashboard may not have direct access to the local
|
|
208
|
+
* event-store file.
|
|
209
|
+
*
|
|
210
|
+
* Note: This is a push-side bridge. The subscriber interface allows
|
|
211
|
+
* the dashboard to subscribe to events that are forwarded through the
|
|
212
|
+
* control plane, enabling the same SSE streaming pattern as local mode.
|
|
213
|
+
*/
|
|
214
|
+
export function createControlPlaneEventSubscriber(
|
|
215
|
+
_options: ControlPlaneEventSubscriberOptions,
|
|
216
|
+
): EventSubscriber {
|
|
217
|
+
return {
|
|
218
|
+
subscribe(
|
|
219
|
+
_filter: ReplayFilter,
|
|
220
|
+
_callback: (event: KernelEvent) => void | Promise<void>,
|
|
221
|
+
): Disposable {
|
|
222
|
+
// The control plane event subscriber provides the interface contract
|
|
223
|
+
// for dashboard integration. The actual polling/streaming implementation
|
|
224
|
+
// will be wired when the control plane SDK exposes a pull/subscribe
|
|
225
|
+
// endpoint for kernel events.
|
|
226
|
+
return {
|
|
227
|
+
dispose: () => {
|
|
228
|
+
// Cleanup when subscription is no longer needed.
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import {
|
|
6
|
+
KERNEL_EVENT_KINDS,
|
|
7
|
+
type Disposable,
|
|
8
|
+
type KernelEvent,
|
|
9
|
+
type ReplayFilter,
|
|
10
|
+
type ToolTraceEntry,
|
|
11
|
+
} from '@lumenflow/kernel';
|
|
12
|
+
|
|
13
|
+
const HTTP_METHOD = {
|
|
14
|
+
GET: 'GET',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const HTTP_STATUS = {
|
|
18
|
+
OK: 200,
|
|
19
|
+
NOT_FOUND: 404,
|
|
20
|
+
METHOD_NOT_ALLOWED: 405,
|
|
21
|
+
NOT_IMPLEMENTED: 501,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
const HEADER = {
|
|
25
|
+
CACHE_CONTROL: 'cache-control',
|
|
26
|
+
CONNECTION: 'connection',
|
|
27
|
+
CONTENT_TYPE: 'content-type',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
const HEADER_VALUE = {
|
|
31
|
+
CACHE_CONTROL: 'no-cache',
|
|
32
|
+
CONNECTION: 'keep-alive',
|
|
33
|
+
EVENT_STREAM: 'text/event-stream; charset=utf-8',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
const JSON_RESPONSE_KEY_ERROR = 'error';
|
|
37
|
+
const JSON_RESPONSE_KEY_MESSAGE = 'message';
|
|
38
|
+
const SSE_DATA_PREFIX = 'data: ';
|
|
39
|
+
const SSE_DOUBLE_NEWLINE = '\n\n';
|
|
40
|
+
const SSE_HEARTBEAT_COMMENT = ':heartbeat\n\n';
|
|
41
|
+
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
42
|
+
const SEARCH_PARAM = {
|
|
43
|
+
KIND: 'kind',
|
|
44
|
+
SINCE_TIMESTAMP: 'sinceTimestamp',
|
|
45
|
+
UNTIL_TIMESTAMP: 'untilTimestamp',
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
const REPLAY_KIND_VALUES = new Set<KernelEvent['kind']>(
|
|
49
|
+
Object.values(KERNEL_EVENT_KINDS) as KernelEvent['kind'][],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* StreamEvent envelope: discriminated union wrapping either a KernelEvent
|
|
54
|
+
* (source: 'kernel') or a ToolTraceEntry (source: 'evidence').
|
|
55
|
+
*
|
|
56
|
+
* Option B from WU-1918 spec notes.
|
|
57
|
+
*/
|
|
58
|
+
export type StreamEvent =
|
|
59
|
+
| { source: 'kernel'; event: KernelEvent }
|
|
60
|
+
| { source: 'evidence'; trace: ToolTraceEntry };
|
|
61
|
+
|
|
62
|
+
export interface EventSubscriber {
|
|
63
|
+
subscribe(
|
|
64
|
+
filter: ReplayFilter,
|
|
65
|
+
callback: (event: KernelEvent) => void | Promise<void>,
|
|
66
|
+
): Disposable;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TraceSubscriber {
|
|
70
|
+
subscribe(taskId: string, callback: (trace: ToolTraceEntry) => void): Disposable;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface EventStreamRouterOptions {
|
|
74
|
+
traceSubscriber?: TraceSubscriber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface EventStreamRouter {
|
|
78
|
+
handleRequest(
|
|
79
|
+
request: IncomingMessage,
|
|
80
|
+
response: ServerResponse<IncomingMessage>,
|
|
81
|
+
routeSegments: string[],
|
|
82
|
+
searchParams: URLSearchParams,
|
|
83
|
+
): Promise<boolean>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeJsonError(
|
|
87
|
+
response: ServerResponse<IncomingMessage>,
|
|
88
|
+
statusCode: number,
|
|
89
|
+
message: string,
|
|
90
|
+
): void {
|
|
91
|
+
response.statusCode = statusCode;
|
|
92
|
+
response.setHeader(HEADER.CONTENT_TYPE, 'application/json; charset=utf-8');
|
|
93
|
+
response.end(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
[JSON_RESPONSE_KEY_ERROR]: {
|
|
96
|
+
[JSON_RESPONSE_KEY_MESSAGE]: message,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toKindFilter(searchParams: URLSearchParams): ReplayFilter['kind'] {
|
|
103
|
+
const values = searchParams
|
|
104
|
+
.getAll(SEARCH_PARAM.KIND)
|
|
105
|
+
.map((value) => value.trim())
|
|
106
|
+
.filter((value): value is KernelEvent['kind'] => {
|
|
107
|
+
return value.length > 0 && REPLAY_KIND_VALUES.has(value as KernelEvent['kind']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (values.length === 0) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (values.length === 1) {
|
|
114
|
+
return values[0];
|
|
115
|
+
}
|
|
116
|
+
return values;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toReplayFilter(taskId: string, searchParams: URLSearchParams): ReplayFilter {
|
|
120
|
+
const sinceTimestamp = searchParams.get(SEARCH_PARAM.SINCE_TIMESTAMP) ?? undefined;
|
|
121
|
+
const untilTimestamp = searchParams.get(SEARCH_PARAM.UNTIL_TIMESTAMP) ?? undefined;
|
|
122
|
+
return {
|
|
123
|
+
taskId,
|
|
124
|
+
kind: toKindFilter(searchParams),
|
|
125
|
+
sinceTimestamp,
|
|
126
|
+
untilTimestamp,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeEventStreamHeaders(response: ServerResponse<IncomingMessage>): void {
|
|
131
|
+
response.statusCode = HTTP_STATUS.OK;
|
|
132
|
+
response.setHeader(HEADER.CONTENT_TYPE, HEADER_VALUE.EVENT_STREAM);
|
|
133
|
+
response.setHeader(HEADER.CACHE_CONTROL, HEADER_VALUE.CACHE_CONTROL);
|
|
134
|
+
response.setHeader(HEADER.CONNECTION, HEADER_VALUE.CONNECTION);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeStreamEvent(response: ServerResponse<IncomingMessage>, envelope: StreamEvent): void {
|
|
138
|
+
const payload = `${SSE_DATA_PREFIX}${JSON.stringify(envelope)}${SSE_DOUBLE_NEWLINE}`;
|
|
139
|
+
response.write(payload);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createEventStreamRouter(
|
|
143
|
+
eventSubscriber?: EventSubscriber,
|
|
144
|
+
options?: EventStreamRouterOptions,
|
|
145
|
+
): EventStreamRouter {
|
|
146
|
+
return {
|
|
147
|
+
async handleRequest(
|
|
148
|
+
request: IncomingMessage,
|
|
149
|
+
response: ServerResponse<IncomingMessage>,
|
|
150
|
+
routeSegments: string[],
|
|
151
|
+
searchParams: URLSearchParams,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
if (routeSegments.length !== 1) {
|
|
154
|
+
writeJsonError(response, HTTP_STATUS.NOT_FOUND, 'Route not found.');
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if ((request.method ?? '') !== HTTP_METHOD.GET) {
|
|
159
|
+
writeJsonError(response, HTTP_STATUS.METHOD_NOT_ALLOWED, 'Unsupported method.');
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!eventSubscriber) {
|
|
164
|
+
writeJsonError(response, HTTP_STATUS.NOT_IMPLEMENTED, 'Event streaming is unavailable.');
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const taskId = routeSegments[0] ?? '';
|
|
169
|
+
const filter = toReplayFilter(taskId, searchParams);
|
|
170
|
+
|
|
171
|
+
writeEventStreamHeaders(response);
|
|
172
|
+
|
|
173
|
+
let isDisposed = false;
|
|
174
|
+
|
|
175
|
+
const eventSub = eventSubscriber.subscribe(filter, async (event) => {
|
|
176
|
+
if (isDisposed) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
writeStreamEvent(response, { source: 'kernel', event });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
let traceSub: Disposable | undefined;
|
|
183
|
+
if (options?.traceSubscriber) {
|
|
184
|
+
traceSub = options.traceSubscriber.subscribe(taskId, (trace) => {
|
|
185
|
+
if (isDisposed) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
writeStreamEvent(response, { source: 'evidence', trace });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const heartbeatTimer = setInterval(() => {
|
|
193
|
+
if (isDisposed) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
response.write(SSE_HEARTBEAT_COMMENT);
|
|
197
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
198
|
+
|
|
199
|
+
const dispose = (): void => {
|
|
200
|
+
if (isDisposed) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
isDisposed = true;
|
|
204
|
+
clearInterval(heartbeatTimer);
|
|
205
|
+
eventSub.dispose();
|
|
206
|
+
traceSub?.dispose();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
request.on('close', dispose);
|
|
210
|
+
response.on('close', dispose);
|
|
211
|
+
response.on('finish', dispose);
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
package/http/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export * from './server.js';
|
|
5
|
+
export * from './ag-ui-adapter.js';
|
|
6
|
+
export * from './run-agent.js';
|
|
7
|
+
export * from './tool-api.js';
|
|
8
|
+
export * from './tool-discovery.js';
|
|
9
|
+
export * from './control-plane-event-subscriber.js';
|
|
10
|
+
export * from './sidecar-entry.js';
|