open-managed-agents 0.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 +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionEventBroadcaster — replay-then-tail fanout over a session's event log.
|
|
3
|
+
*
|
|
4
|
+
* On publish: persist via EventStore, then push to any live subscribers on this
|
|
5
|
+
* session (persist-before-publish, ADR 0007 Pattern 2).
|
|
6
|
+
*
|
|
7
|
+
* On subscribe: register a live-event listener BEFORE replaying durable history,
|
|
8
|
+
* buffer live events while replay runs, dedupe by event ID on drain, then yield
|
|
9
|
+
* live events until aborted (replay-then-tail, ADR 0007 Pattern 1). Replay is
|
|
10
|
+
* paginated; live queue is bounded — on overflow we drop the queue and re-drain
|
|
11
|
+
* from the store via cursor. Mirrors Flue's bounded-buffer + refetch shape.
|
|
12
|
+
*
|
|
13
|
+
* Race we're guarding against:
|
|
14
|
+
* t0: subscriber arrives with lastSeenId X
|
|
15
|
+
* t1: subscriber registers live listener
|
|
16
|
+
* t2: a new event Y is published (lands in live queue)
|
|
17
|
+
* t3: store.list() reads — may include Y, may not, depending on timing
|
|
18
|
+
* Without buffering live events during replay and dedup-on-drain, Y is either
|
|
19
|
+
* delivered twice or not at all.
|
|
20
|
+
*
|
|
21
|
+
* Overflow recovery: if the live queue exceeds MAX_BUFFER while the subscriber
|
|
22
|
+
* is suspended (slow consumer / long replay), we drop the buffer and re-drain
|
|
23
|
+
* from store starting after the last-yielded ID. This bounds memory usage at
|
|
24
|
+
* MAX_BUFFER events per subscription regardless of publish rate.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_PAGE_SIZE = 500;
|
|
27
|
+
const DEFAULT_MAX_BUFFER = 10_000;
|
|
28
|
+
export class SessionEventBroadcaster {
|
|
29
|
+
store;
|
|
30
|
+
subscribers = new Map();
|
|
31
|
+
constructor(store) {
|
|
32
|
+
this.store = store;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fan out already-persisted events to live subscribers for their sessions.
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: this method does not persist. Callers that need durability must
|
|
38
|
+
* persist first (e.g. `appendBatch`) before notifying.
|
|
39
|
+
*/
|
|
40
|
+
publishPersisted(events) {
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
const subs = this.subscribers.get(subscriptionKey(event.workspace_id, event.session_id));
|
|
43
|
+
if (!subs)
|
|
44
|
+
continue;
|
|
45
|
+
for (const sub of subs) {
|
|
46
|
+
sub.push(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
closeSession(workspaceId, sessionId) {
|
|
51
|
+
const subs = this.subscribers.get(subscriptionKey(workspaceId, sessionId));
|
|
52
|
+
if (!subs)
|
|
53
|
+
return;
|
|
54
|
+
for (const sub of subs) {
|
|
55
|
+
sub.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* AsyncIterable yielding events for `sessionId` in stable ID order, deduped.
|
|
60
|
+
* Replays history with `id > lastSeenId` via paginated cursor, then tails
|
|
61
|
+
* live events until the supplied signal aborts.
|
|
62
|
+
*/
|
|
63
|
+
async *subscribe(workspaceId, sessionId, opts = {}) {
|
|
64
|
+
const pageSize = positiveIntegerOrThrow(opts.pageSize ?? DEFAULT_PAGE_SIZE, "pageSize");
|
|
65
|
+
const maxBuffer = positiveIntegerOrThrow(opts.maxBuffer ?? DEFAULT_MAX_BUFFER, "maxBuffer");
|
|
66
|
+
const liveQueue = [];
|
|
67
|
+
let overflowed = false;
|
|
68
|
+
let closed = false;
|
|
69
|
+
let wake = null;
|
|
70
|
+
const wakeSubscriber = () => {
|
|
71
|
+
if (!wake)
|
|
72
|
+
return;
|
|
73
|
+
const w = wake;
|
|
74
|
+
wake = null;
|
|
75
|
+
w();
|
|
76
|
+
};
|
|
77
|
+
const sub = {
|
|
78
|
+
push(event) {
|
|
79
|
+
if (overflowed) {
|
|
80
|
+
// Already overflowed; drop. Will recover via store-drain.
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
liveQueue.push(event);
|
|
84
|
+
if (liveQueue.length > maxBuffer) {
|
|
85
|
+
// Drop the entire buffer; we'll refetch from the store using the
|
|
86
|
+
// last-yielded ID as the cursor. This bounds memory at maxBuffer.
|
|
87
|
+
overflowed = true;
|
|
88
|
+
liveQueue.length = 0;
|
|
89
|
+
}
|
|
90
|
+
wakeSubscriber();
|
|
91
|
+
},
|
|
92
|
+
close() {
|
|
93
|
+
closed = true;
|
|
94
|
+
wakeSubscriber();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
// CRITICAL: register live listener BEFORE replaying. Any event published
|
|
98
|
+
// during replay lands in liveQueue (or trips overflow); dedup by
|
|
99
|
+
// `id <= lastYieldedId` handles the overlap.
|
|
100
|
+
this.addSubscriber(workspaceId, sessionId, sub);
|
|
101
|
+
const onAbort = () => {
|
|
102
|
+
wakeSubscriber();
|
|
103
|
+
};
|
|
104
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
105
|
+
let lastYieldedId = opts.lastSeenId;
|
|
106
|
+
const aborted = () => opts.signal?.aborted ?? false;
|
|
107
|
+
/**
|
|
108
|
+
* Drain the store from current `lastYieldedId` to end, yielding each event.
|
|
109
|
+
* Paginated — keeps loading pages until a partial page (or empty) tells us
|
|
110
|
+
* we've caught up.
|
|
111
|
+
*/
|
|
112
|
+
const store = this.store;
|
|
113
|
+
function* drainStore() {
|
|
114
|
+
while (true) {
|
|
115
|
+
const page = store.list(workspaceId, sessionId, {
|
|
116
|
+
afterId: lastYieldedId,
|
|
117
|
+
limit: pageSize,
|
|
118
|
+
});
|
|
119
|
+
if (page.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
for (const event of page) {
|
|
122
|
+
yield event;
|
|
123
|
+
}
|
|
124
|
+
if (page.length < pageSize)
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
// 1. Initial replay (paginated).
|
|
130
|
+
for (const event of drainStore()) {
|
|
131
|
+
if (aborted())
|
|
132
|
+
return;
|
|
133
|
+
lastYieldedId = event.id;
|
|
134
|
+
yield event;
|
|
135
|
+
}
|
|
136
|
+
// 2. Drain live + tail with overflow recovery.
|
|
137
|
+
while (true) {
|
|
138
|
+
if (aborted())
|
|
139
|
+
return;
|
|
140
|
+
// Handle overflow: drop buffer, re-drain from store.
|
|
141
|
+
if (overflowed) {
|
|
142
|
+
overflowed = false;
|
|
143
|
+
liveQueue.length = 0;
|
|
144
|
+
for (const event of drainStore()) {
|
|
145
|
+
if (aborted())
|
|
146
|
+
return;
|
|
147
|
+
// dedup-by-cursor: drainStore already starts after lastYieldedId,
|
|
148
|
+
// so every event is new.
|
|
149
|
+
lastYieldedId = event.id;
|
|
150
|
+
yield event;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Drain whatever's currently in the live queue.
|
|
155
|
+
while (liveQueue.length > 0) {
|
|
156
|
+
if (aborted())
|
|
157
|
+
return;
|
|
158
|
+
if (overflowed)
|
|
159
|
+
break; // race: overflow happened during this drain
|
|
160
|
+
const event = liveQueue.shift();
|
|
161
|
+
// Dedup against the cursor — anything we've already yielded has
|
|
162
|
+
// id <= lastYieldedId.
|
|
163
|
+
if (lastYieldedId !== undefined && event.id <= lastYieldedId) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
lastYieldedId = event.id;
|
|
167
|
+
yield event;
|
|
168
|
+
}
|
|
169
|
+
if (overflowed)
|
|
170
|
+
continue; // re-enter overflow handler
|
|
171
|
+
if (aborted())
|
|
172
|
+
return;
|
|
173
|
+
if (closed)
|
|
174
|
+
return;
|
|
175
|
+
// Sleep until next event or abort.
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
wake = resolve;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
this.removeSubscriber(workspaceId, sessionId, sub);
|
|
183
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** For tests/introspection. Not part of the public broadcaster contract. */
|
|
187
|
+
subscriberCount(sessionId, workspaceId = "wrk_default") {
|
|
188
|
+
return this.subscribers.get(subscriptionKey(workspaceId, sessionId))?.size ?? 0;
|
|
189
|
+
}
|
|
190
|
+
addSubscriber(workspaceId, sessionId, sub) {
|
|
191
|
+
const key = subscriptionKey(workspaceId, sessionId);
|
|
192
|
+
let set = this.subscribers.get(key);
|
|
193
|
+
if (!set) {
|
|
194
|
+
set = new Set();
|
|
195
|
+
this.subscribers.set(key, set);
|
|
196
|
+
}
|
|
197
|
+
set.add(sub);
|
|
198
|
+
}
|
|
199
|
+
removeSubscriber(workspaceId, sessionId, sub) {
|
|
200
|
+
const key = subscriptionKey(workspaceId, sessionId);
|
|
201
|
+
const set = this.subscribers.get(key);
|
|
202
|
+
if (!set)
|
|
203
|
+
return;
|
|
204
|
+
set.delete(sub);
|
|
205
|
+
if (set.size === 0)
|
|
206
|
+
this.subscribers.delete(key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function subscriptionKey(workspaceId, sessionId) {
|
|
210
|
+
return JSON.stringify([workspaceId, sessionId]);
|
|
211
|
+
}
|
|
212
|
+
function positiveIntegerOrThrow(value, name) {
|
|
213
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
214
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { newEventId } from "../../types/events.js";
|
|
2
|
+
import { invalidRequest } from "../errors.js";
|
|
3
|
+
import { MAX_EVENT_PAYLOAD_BYTES } from "./constants.js";
|
|
4
|
+
export function materializePersistedEvents(workspaceId, sessionId, drafts, now) {
|
|
5
|
+
return drafts.map((draft) => {
|
|
6
|
+
const payloadJson = JSON.stringify(draft.payload);
|
|
7
|
+
if (new TextEncoder().encode(payloadJson).byteLength > MAX_EVENT_PAYLOAD_BYTES) {
|
|
8
|
+
throw invalidRequest(`Serialized event payload exceeds ${MAX_EVENT_PAYLOAD_BYTES} bytes`);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
id: newEventId(),
|
|
12
|
+
workspace_id: workspaceId,
|
|
13
|
+
session_id: sessionId,
|
|
14
|
+
type: draft.type,
|
|
15
|
+
processed_at: now,
|
|
16
|
+
payload: draft.payload,
|
|
17
|
+
created_at: now,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function persistAndPublish(store, broadcaster, events) {
|
|
22
|
+
// Keep persist-then-notify in the same sync tick. Do not `await` between
|
|
23
|
+
// appendBatch and publishPersisted; that would open a replay gap.
|
|
24
|
+
store.appendBatch(events);
|
|
25
|
+
broadcaster.publishPersisted(events);
|
|
26
|
+
}
|
|
27
|
+
export function persistRuntimeChangesAndPublish(store, broadcaster, events, changes) {
|
|
28
|
+
// Same persist-then-notify invariant as persistAndPublish, with runtime
|
|
29
|
+
// ledger mutations committed in the same SQLite transaction as the events.
|
|
30
|
+
store.appendBatchWithRuntimeChanges(events, changes);
|
|
31
|
+
broadcaster.publishPersisted(events);
|
|
32
|
+
}
|
|
33
|
+
export function persistRuntimeChangesCompleteIdempotencyAndPublish(store, broadcaster, events, changes, completion) {
|
|
34
|
+
// The idempotency response must commit in the same transaction as the domain
|
|
35
|
+
// event/runtime writes, or a crash can leave a committed side effect with no
|
|
36
|
+
// replayable response.
|
|
37
|
+
store.appendBatchWithRuntimeChangesAndCompleteIdempotency(events, changes, completion);
|
|
38
|
+
broadcaster.publishPersisted(events);
|
|
39
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { isJsonObject, isJsonValue, } from "../../types/json.js";
|
|
2
|
+
import { invalidRequest } from "../errors.js";
|
|
3
|
+
import { MAX_EVENTS_PER_REQUEST } from "./constants.js";
|
|
4
|
+
import { toManagedAgentsEvent } from "./types.js";
|
|
5
|
+
const SUPPORTED_USER_EVENT_TYPES = new Set([
|
|
6
|
+
"user.message",
|
|
7
|
+
"user.interrupt",
|
|
8
|
+
"user.custom_tool_result",
|
|
9
|
+
"user.tool_confirmation",
|
|
10
|
+
]);
|
|
11
|
+
export function parseSendRequest(input) {
|
|
12
|
+
const obj = objectInput(input);
|
|
13
|
+
const value = obj.events;
|
|
14
|
+
if (!Array.isArray(value)) {
|
|
15
|
+
throw invalidRequest("`events` must be a non-empty array");
|
|
16
|
+
}
|
|
17
|
+
if (value.length === 0) {
|
|
18
|
+
throw invalidRequest("`events` must be a non-empty array");
|
|
19
|
+
}
|
|
20
|
+
if (value.length > MAX_EVENTS_PER_REQUEST) {
|
|
21
|
+
throw invalidRequest(`\`events\` must contain at most ${MAX_EVENTS_PER_REQUEST} items`);
|
|
22
|
+
}
|
|
23
|
+
const events = value.map((item, index) => parseUserEvent(item, index));
|
|
24
|
+
rejectMixedInterruptAndMessage(events);
|
|
25
|
+
return { events };
|
|
26
|
+
}
|
|
27
|
+
export function eventPayload(event) {
|
|
28
|
+
const { type: _type, ...payload } = event;
|
|
29
|
+
return payload;
|
|
30
|
+
}
|
|
31
|
+
export function toSendResponseEvent(row) {
|
|
32
|
+
if (!row)
|
|
33
|
+
throw new Error("Persisted event row missing");
|
|
34
|
+
const event = toManagedAgentsEvent(row);
|
|
35
|
+
if (row.type === "user.tool_confirmation") {
|
|
36
|
+
return { ...event, processed_at: null };
|
|
37
|
+
}
|
|
38
|
+
return event;
|
|
39
|
+
}
|
|
40
|
+
export function sameToolConfirmation(completed, event) {
|
|
41
|
+
return (completed.result === event.result &&
|
|
42
|
+
(completed.denyMessage ?? null) === (event.deny_message ?? null));
|
|
43
|
+
}
|
|
44
|
+
export function sameCustomToolResult(left, right) {
|
|
45
|
+
return (left.custom_tool_use_id === right.custom_tool_use_id &&
|
|
46
|
+
JSON.stringify(left.content ?? null) === JSON.stringify(right.content ?? null) &&
|
|
47
|
+
(left.is_error ?? false) === (right.is_error ?? false));
|
|
48
|
+
}
|
|
49
|
+
export function sameCustomToolResultPayload(payload, event) {
|
|
50
|
+
return (payload.custom_tool_use_id === event.custom_tool_use_id &&
|
|
51
|
+
JSON.stringify(payload.content ?? null) === JSON.stringify(event.content ?? null) &&
|
|
52
|
+
(payload.is_error ?? false) === (event.is_error ?? false));
|
|
53
|
+
}
|
|
54
|
+
export function hasToolResultForToolUseId(rows, toolUseId) {
|
|
55
|
+
return rows.some((row) => (row.type === "agent.tool_result" &&
|
|
56
|
+
row.payload.tool_use_id === toolUseId) ||
|
|
57
|
+
(row.type === "agent.mcp_tool_result" &&
|
|
58
|
+
row.payload.mcp_tool_use_id === toolUseId));
|
|
59
|
+
}
|
|
60
|
+
export function lostToolConfirmationPayload(toolUseId) {
|
|
61
|
+
return {
|
|
62
|
+
tool_use_id: toolUseId,
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `Tool confirmation ${toolUseId} was accepted, but runtime state is no longer available and the builtin tool execution outcome is unknown.`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
is_error: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function lostMcpToolConfirmationPayload(toolUseId) {
|
|
73
|
+
return {
|
|
74
|
+
mcp_tool_use_id: toolUseId,
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Tool confirmation ${toolUseId} was accepted, but runtime state is no longer available and the MCP tool execution outcome is unknown.`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
is_error: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function objectInput(input) {
|
|
85
|
+
if (!isJsonObject(input)) {
|
|
86
|
+
throw invalidRequest("Request body must be a JSON object");
|
|
87
|
+
}
|
|
88
|
+
return input;
|
|
89
|
+
}
|
|
90
|
+
export function nonEmptyString(value, field) {
|
|
91
|
+
if (typeof value === "string" && value.length > 0)
|
|
92
|
+
return value;
|
|
93
|
+
throw invalidRequest(`\`${field}\` must be a non-empty string`);
|
|
94
|
+
}
|
|
95
|
+
function rejectMixedInterruptAndMessage(events) {
|
|
96
|
+
const hasInterrupt = events.some((event) => event.type === "user.interrupt");
|
|
97
|
+
if (!hasInterrupt)
|
|
98
|
+
return;
|
|
99
|
+
const hasMessage = events.some((event) => event.type === "user.message");
|
|
100
|
+
if (!hasMessage)
|
|
101
|
+
return;
|
|
102
|
+
throw invalidRequest("`events` cannot mix user.interrupt and user.message in one request");
|
|
103
|
+
}
|
|
104
|
+
function parseUserEvent(input, index) {
|
|
105
|
+
const event = objectInput(input);
|
|
106
|
+
if ("session_id" in event) {
|
|
107
|
+
throw invalidRequest(`\`events[${index}].session_id\` is not allowed; session ID comes from the URL path`);
|
|
108
|
+
}
|
|
109
|
+
const type = nonEmptyString(event.type, `events[${index}].type`);
|
|
110
|
+
if (!SUPPORTED_USER_EVENT_TYPES.has(type)) {
|
|
111
|
+
throw invalidRequest(`\`events[${index}].type\` must be one of user.message, user.interrupt, user.custom_tool_result, user.tool_confirmation`);
|
|
112
|
+
}
|
|
113
|
+
if (!isJsonValue(event)) {
|
|
114
|
+
throw invalidRequest(`\`events[${index}]\` must be JSON-compatible`);
|
|
115
|
+
}
|
|
116
|
+
if (type === "user.message") {
|
|
117
|
+
const content = parseContentArray(event.content, `events[${index}].content`);
|
|
118
|
+
return { type: "user.message", content };
|
|
119
|
+
}
|
|
120
|
+
if (type === "user.interrupt") {
|
|
121
|
+
return { type: "user.interrupt" };
|
|
122
|
+
}
|
|
123
|
+
if (type === "user.custom_tool_result") {
|
|
124
|
+
const customToolUseId = nonEmptyString(event.custom_tool_use_id, `events[${index}].custom_tool_use_id`);
|
|
125
|
+
const contentValue = event.content;
|
|
126
|
+
return {
|
|
127
|
+
type: "user.custom_tool_result",
|
|
128
|
+
custom_tool_use_id: customToolUseId,
|
|
129
|
+
...(contentValue === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: {
|
|
132
|
+
content: parseContentArray(contentValue, `events[${index}].content`),
|
|
133
|
+
}),
|
|
134
|
+
...optionalBooleanSpread(event.is_error, `events[${index}].is_error`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const toolUseId = nonEmptyString(event.tool_use_id, `events[${index}].tool_use_id`);
|
|
138
|
+
const result = event.result;
|
|
139
|
+
if (result !== "allow" && result !== "deny") {
|
|
140
|
+
throw invalidRequest(`\`events[${index}].result\` must be \`allow\` or \`deny\``);
|
|
141
|
+
}
|
|
142
|
+
const denyMessage = event.deny_message;
|
|
143
|
+
if (result === "allow" && denyMessage !== undefined) {
|
|
144
|
+
throw invalidRequest(`\`events[${index}].deny_message\` is only valid when result is \`deny\``);
|
|
145
|
+
}
|
|
146
|
+
if (result === "deny" &&
|
|
147
|
+
denyMessage !== undefined &&
|
|
148
|
+
denyMessage !== null &&
|
|
149
|
+
typeof denyMessage !== "string") {
|
|
150
|
+
throw invalidRequest(`\`events[${index}].deny_message\` must be a string or null`);
|
|
151
|
+
}
|
|
152
|
+
const normalizedDenyMessage = denyMessage === undefined || denyMessage === null || typeof denyMessage === "string"
|
|
153
|
+
? denyMessage
|
|
154
|
+
: undefined;
|
|
155
|
+
return {
|
|
156
|
+
type: "user.tool_confirmation",
|
|
157
|
+
tool_use_id: toolUseId,
|
|
158
|
+
result,
|
|
159
|
+
...(normalizedDenyMessage === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { deny_message: normalizedDenyMessage }),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function parseContentArray(input, field) {
|
|
165
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
166
|
+
throw invalidRequest(`\`${field}\` must be a non-empty array`);
|
|
167
|
+
}
|
|
168
|
+
return input.map((block, index) => parseContentBlock(block, `${field}[${index}]`));
|
|
169
|
+
}
|
|
170
|
+
function parseContentBlock(input, field) {
|
|
171
|
+
const block = objectInput(input);
|
|
172
|
+
if (!isJsonValue(block)) {
|
|
173
|
+
throw invalidRequest(`\`${field}\` must be JSON-compatible`);
|
|
174
|
+
}
|
|
175
|
+
const type = nonEmptyString(block.type, `${field}.type`);
|
|
176
|
+
if (type === "text") {
|
|
177
|
+
return {
|
|
178
|
+
type,
|
|
179
|
+
text: nonEmptyString(block.text, `${field}.text`),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return block;
|
|
183
|
+
}
|
|
184
|
+
function optionalBooleanSpread(value, field) {
|
|
185
|
+
if (value === undefined)
|
|
186
|
+
return {};
|
|
187
|
+
if (typeof value === "boolean")
|
|
188
|
+
return { is_error: value };
|
|
189
|
+
throw invalidRequest(`\`${field}\` must be a boolean`);
|
|
190
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { parseJsonBody, parseLimit, parseOrder } from "../http.js";
|
|
3
|
+
import { invalidRequest } from "../errors.js";
|
|
4
|
+
import { requestFingerprint, validateIdempotencyKey, } from "../request-idempotency.js";
|
|
5
|
+
import { workspaceIdFrom } from "../workspace.js";
|
|
6
|
+
import { sseEventFrame } from "./sse.js";
|
|
7
|
+
export function sessionEventsRoutes(service, admission) {
|
|
8
|
+
const app = new Hono();
|
|
9
|
+
app.post("/", async (c) => {
|
|
10
|
+
const sessionId = requiredSessionId(c.req.param("sessionId"));
|
|
11
|
+
const idempotencyKey = c.req.header("idempotency-key");
|
|
12
|
+
if (idempotencyKey !== undefined) {
|
|
13
|
+
const rawBody = new Uint8Array(await c.req.raw.arrayBuffer());
|
|
14
|
+
const body = parseJsonBytes(rawBody);
|
|
15
|
+
const method = c.req.method.toUpperCase();
|
|
16
|
+
const concretePath = new URL(c.req.url).pathname;
|
|
17
|
+
const response = service.sendIdempotent(workspaceIdFrom(c), sessionId, body, {
|
|
18
|
+
method,
|
|
19
|
+
concretePath,
|
|
20
|
+
key: validateIdempotencyKey(idempotencyKey),
|
|
21
|
+
routeLabel: "POST /v1/sessions/{session_id}/events",
|
|
22
|
+
fingerprintSha256: requestFingerprint(method, concretePath, rawBody),
|
|
23
|
+
}, {
|
|
24
|
+
signal: c.req.raw.signal,
|
|
25
|
+
requestId: c.get("requestId"),
|
|
26
|
+
});
|
|
27
|
+
return jsonResponse(response.body, response.status, c.get("requestId"), response.headers);
|
|
28
|
+
}
|
|
29
|
+
const body = await parseJsonBody(c.req);
|
|
30
|
+
return c.json({
|
|
31
|
+
data: service.send(workspaceIdFrom(c), sessionId, body, {
|
|
32
|
+
signal: c.req.raw.signal,
|
|
33
|
+
}),
|
|
34
|
+
}, 200);
|
|
35
|
+
});
|
|
36
|
+
app.get("/", (c) => {
|
|
37
|
+
const sessionId = requiredSessionId(c.req.param("sessionId"));
|
|
38
|
+
const limit = parseLimit(c.req.query("limit"));
|
|
39
|
+
const page = parsePage(c.req.query("page"));
|
|
40
|
+
const order = parseOrder(c.req.query("order"));
|
|
41
|
+
const types = parseTypesQuery(c.req.url);
|
|
42
|
+
return c.json(service.list(workspaceIdFrom(c), sessionId, {
|
|
43
|
+
...(limit === undefined ? {} : { limit }),
|
|
44
|
+
...(page === undefined ? {} : { page }),
|
|
45
|
+
...(order === undefined ? {} : { order }),
|
|
46
|
+
...(types.length === 0 ? {} : { types }),
|
|
47
|
+
}), 200);
|
|
48
|
+
});
|
|
49
|
+
app.get("/stream", (c) => {
|
|
50
|
+
const sessionId = requiredSessionId(c.req.param("sessionId"));
|
|
51
|
+
rejectUnsupportedEventDeltas(c.req.url);
|
|
52
|
+
// 0113 D9: the stream's cost is its lifetime (a bounded live queue per
|
|
53
|
+
// subscription), so the slot is held until the stream closes.
|
|
54
|
+
const releaseStream = admission?.sseStreams.acquire(workspaceIdFrom(c));
|
|
55
|
+
const abortController = new AbortController();
|
|
56
|
+
c.req.raw.signal.addEventListener("abort", () => abortController.abort(), {
|
|
57
|
+
once: true,
|
|
58
|
+
});
|
|
59
|
+
let events;
|
|
60
|
+
try {
|
|
61
|
+
events = service.stream(workspaceIdFrom(c), sessionId, {
|
|
62
|
+
lastEventId: c.req.header("last-event-id"),
|
|
63
|
+
signal: abortController.signal,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
releaseStream?.();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const requestId = c.get("requestId");
|
|
71
|
+
return new Response(toSseBody(events, abortController, releaseStream), {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "text/event-stream",
|
|
75
|
+
"cache-control": "no-cache",
|
|
76
|
+
connection: "keep-alive",
|
|
77
|
+
"request-id": requestId,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return app;
|
|
82
|
+
}
|
|
83
|
+
function requiredSessionId(value) {
|
|
84
|
+
if (value === undefined || value.length === 0) {
|
|
85
|
+
throw invalidRequest("Session ID path parameter is required");
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function parseJsonBytes(bytes) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw invalidRequest("Request body must be valid JSON", String(error));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function jsonResponse(body, status, requestId, extraHeaders = {}) {
|
|
98
|
+
return new Response(JSON.stringify(body), {
|
|
99
|
+
status,
|
|
100
|
+
headers: {
|
|
101
|
+
"content-type": "application/json",
|
|
102
|
+
"request-id": requestId,
|
|
103
|
+
...extraHeaders,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function parsePage(value) {
|
|
108
|
+
if (value === undefined || value.length === 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (!value.startsWith("sevt_")) {
|
|
111
|
+
throw invalidRequest("`page` must be a valid event cursor");
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function parseTypesQuery(url) {
|
|
116
|
+
const params = new URL(url).searchParams;
|
|
117
|
+
const values = params.getAll("types[]");
|
|
118
|
+
return values.filter((value) => value.length > 0);
|
|
119
|
+
}
|
|
120
|
+
function rejectUnsupportedEventDeltas(url) {
|
|
121
|
+
if (new URL(url).searchParams.has("event_deltas[]")) {
|
|
122
|
+
throw invalidRequest("`event_deltas[]` is not supported; OMA streams complete persisted events and emits buffered `agent.message` text");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function toSseBody(events, abortController, onClose) {
|
|
126
|
+
const encoder = new TextEncoder();
|
|
127
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
128
|
+
return new ReadableStream({
|
|
129
|
+
// Demand-gated on purpose: pull() runs only when the consumer has drained
|
|
130
|
+
// the stream's internal queue, so a slow/stalled client suspends the
|
|
131
|
+
// broadcaster iterator and its bounded live-queue overflow→refetch
|
|
132
|
+
// protection holds. An eager start() loop here drains the broadcaster
|
|
133
|
+
// into unbounded response/socket buffering (#127). A rejected pull()
|
|
134
|
+
// errors the stream, matching the previous controller.error() path.
|
|
135
|
+
async pull(controller) {
|
|
136
|
+
let next;
|
|
137
|
+
try {
|
|
138
|
+
next = await iterator.next();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
onClose?.();
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (next.done) {
|
|
145
|
+
onClose?.();
|
|
146
|
+
controller.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
controller.enqueue(encoder.encode(sseEventFrame(next.value)));
|
|
150
|
+
},
|
|
151
|
+
async cancel() {
|
|
152
|
+
onClose?.();
|
|
153
|
+
abortController.abort();
|
|
154
|
+
if (typeof iterator.return === "function") {
|
|
155
|
+
await iterator.return();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|