stageflow 0.36.0 → 0.40.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/README.md +61 -26
- package/dist/agent/claudeAdapter.js +21 -1
- package/dist/agent/claudeTools.d.ts +13 -0
- package/dist/agent/claudeTools.js +98 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +59 -18
- package/dist/agent/piIsolatedMcp.js +14 -2
- package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
- package/dist/agent/piIsolatedMcpProbe.js +16 -30
- package/dist/agent/port.d.ts +5 -0
- package/dist/agent/sandboxPiTools.d.ts +99 -0
- package/dist/agent/sandboxPiTools.js +109 -0
- package/dist/agents/agentId.d.ts +8 -0
- package/dist/agents/agentId.js +27 -0
- package/dist/agents/approvalBroker.d.ts +16 -0
- package/dist/agents/approvalBroker.js +45 -0
- package/dist/agents/authorChat.d.ts +19 -0
- package/dist/agents/authorChat.js +63 -0
- package/dist/agents/chatTurn.d.ts +23 -0
- package/dist/agents/chatTurn.js +77 -0
- package/dist/agents/communications/agentmail.d.ts +19 -0
- package/dist/agents/communications/agentmail.js +46 -0
- package/dist/agents/communications/agentmailListener.d.ts +59 -0
- package/dist/agents/communications/agentmailListener.js +183 -0
- package/dist/agents/communications/allowlist.d.ts +10 -0
- package/dist/agents/communications/allowlist.js +36 -0
- package/dist/agents/communications/channelConnections.d.ts +3 -0
- package/dist/agents/communications/channelConnections.js +51 -0
- package/dist/agents/communications/delivery.d.ts +53 -0
- package/dist/agents/communications/delivery.js +142 -0
- package/dist/agents/communications/email.d.ts +26 -0
- package/dist/agents/communications/email.js +20 -0
- package/dist/agents/communications/identity.d.ts +13 -0
- package/dist/agents/communications/identity.js +18 -0
- package/dist/agents/communications/imessageListener.d.ts +41 -0
- package/dist/agents/communications/imessageListener.js +92 -0
- package/dist/agents/communications/inbound.d.ts +63 -0
- package/dist/agents/communications/inbound.js +143 -0
- package/dist/agents/communications/listener.d.ts +36 -0
- package/dist/agents/communications/listener.js +38 -0
- package/dist/agents/communications/photon.d.ts +19 -0
- package/dist/agents/communications/photon.js +38 -0
- package/dist/agents/communications/port.d.ts +27 -0
- package/dist/agents/communications/port.js +6 -0
- package/dist/agents/communications/slack.d.ts +28 -0
- package/dist/agents/communications/slack.js +65 -0
- package/dist/agents/communications/supervisor.d.ts +36 -0
- package/dist/agents/communications/supervisor.js +96 -0
- package/dist/agents/communications/telegram.d.ts +19 -0
- package/dist/agents/communications/telegram.js +52 -0
- package/dist/agents/definitionSummary.d.ts +13 -0
- package/dist/agents/definitionSummary.js +91 -0
- package/dist/agents/delegate.d.ts +3 -0
- package/dist/agents/delegate.js +8 -0
- package/dist/agents/errors.d.ts +10 -0
- package/dist/agents/errors.js +32 -0
- package/dist/agents/guardrails.d.ts +24 -0
- package/dist/agents/guardrails.js +39 -0
- package/dist/agents/historyFit.d.ts +10 -0
- package/dist/agents/historyFit.js +26 -0
- package/dist/agents/hostContext.d.ts +54 -0
- package/dist/agents/hostContext.js +156 -0
- package/dist/agents/ledgers.d.ts +29 -0
- package/dist/agents/ledgers.js +31 -0
- package/dist/agents/lineDiff.d.ts +12 -0
- package/dist/agents/lineDiff.js +46 -0
- package/dist/agents/ndjson.d.ts +9 -0
- package/dist/agents/ndjson.js +47 -0
- package/dist/agents/otlpExport.d.ts +9 -0
- package/dist/agents/otlpExport.js +58 -0
- package/dist/agents/otlpSink.d.ts +7 -0
- package/dist/agents/otlpSink.js +23 -0
- package/dist/agents/policy.d.ts +31 -0
- package/dist/agents/policy.js +84 -0
- package/dist/agents/projectRoot.d.ts +5 -0
- package/dist/agents/projectRoot.js +15 -0
- package/dist/agents/runtime/piRuntime.d.ts +52 -0
- package/dist/agents/runtime/piRuntime.js +474 -0
- package/dist/agents/runtime/types.d.ts +77 -0
- package/dist/agents/runtime/types.js +26 -0
- package/dist/agents/sandbox.d.ts +17 -0
- package/dist/agents/sandbox.js +10 -0
- package/dist/agents/scheduleSource.d.ts +32 -0
- package/dist/agents/scheduleSource.js +100 -0
- package/dist/agents/service.d.ts +298 -0
- package/dist/agents/service.js +1105 -0
- package/dist/agents/sessions.d.ts +38 -0
- package/dist/agents/sessions.js +321 -0
- package/dist/agents/store.d.ts +277 -0
- package/dist/agents/store.js +643 -0
- package/dist/agents/trace.d.ts +27 -0
- package/dist/agents/trace.js +29 -0
- package/dist/agents/turnLifecycle.d.ts +72 -0
- package/dist/agents/turnLifecycle.js +157 -0
- package/dist/agents/webChat/publicChat.d.ts +36 -0
- package/dist/agents/webChat/publicChat.js +290 -0
- package/dist/agents/webChat/rateLimit.d.ts +15 -0
- package/dist/agents/webChat/rateLimit.js +33 -0
- package/dist/agents/webChat/slug.d.ts +2 -0
- package/dist/agents/webChat/slug.js +9 -0
- package/dist/channels/providers/agentmail.js +14 -1
- package/dist/channels/providers/photonImessage.js +36 -0
- package/dist/channels/providers/slack.js +2 -1
- package/dist/channels/providers/telegram.js +2 -1
- package/dist/channels/types.d.ts +27 -0
- package/dist/cli/agentsCommand.d.ts +14 -0
- package/dist/cli/agentsCommand.js +291 -0
- package/dist/cli.js +9 -0
- package/dist/config/browseCatalog.js +4 -11
- package/dist/config/draftPackage.d.ts +1 -0
- package/dist/config/draftPackage.js +4 -0
- package/dist/config/hostConfig.js +9 -0
- package/dist/config/loadAgent.d.ts +266 -0
- package/dist/config/loadAgent.js +374 -0
- package/dist/config/loadPipeline.js +5 -2
- package/dist/config/loadTask.js +34 -1
- package/dist/config/mcpHostCatalog.d.ts +50 -0
- package/dist/config/mcpHostCatalog.js +405 -0
- package/dist/config/mcpOAuth.d.ts +37 -0
- package/dist/config/mcpOAuth.js +263 -0
- package/dist/config/mcpRedaction.d.ts +42 -0
- package/dist/config/mcpRedaction.js +314 -0
- package/dist/config/mcpToolCache.d.ts +20 -0
- package/dist/config/mcpToolCache.js +43 -0
- package/dist/config/mergePipelineIncludes.d.ts +2 -0
- package/dist/config/mergePipelineIncludes.js +25 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -3
- package/dist/config/resolveStageMcpServers.js +124 -9
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +22 -1
- package/dist/mcp/agentTools.d.ts +8 -0
- package/dist/mcp/agentTools.js +132 -0
- package/dist/mcp/catalogTools.js +17 -10
- package/dist/mcp/deps.d.ts +6 -0
- package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
- package/dist/mcp/pipelineAuthorSupport.js +10 -0
- package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
- package/dist/mcp/pipelineAuthorTools.js +109 -0
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/triggerTools.js +32 -0
- package/dist/memory/access.d.ts +36 -0
- package/dist/memory/access.js +116 -0
- package/dist/memory/agentIssues.d.ts +3 -0
- package/dist/memory/agentIssues.js +23 -0
- package/dist/memory/bashDeny.d.ts +6 -0
- package/dist/memory/bashDeny.js +35 -0
- package/dist/memory/host.d.ts +51 -0
- package/dist/memory/host.js +394 -0
- package/dist/memory/ids.d.ts +15 -0
- package/dist/memory/ids.js +31 -0
- package/dist/memory/worker.d.ts +1 -0
- package/dist/memory/worker.js +84 -0
- package/dist/models/catalog.d.ts +113 -0
- package/dist/models/catalog.js +238 -0
- package/dist/models/response.d.ts +29 -0
- package/dist/models/response.js +25 -0
- package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
- package/dist/operatorAgent/agentAuthorContext.js +32 -0
- package/dist/operatorAgent/fakeHost.js +2 -0
- package/dist/operatorAgent/host.js +6 -3
- package/dist/operatorAgent/index.d.ts +4 -0
- package/dist/operatorAgent/index.js +4 -0
- package/dist/operatorAgent/piModel.js +2 -0
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
- package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
- package/dist/operatorAgent/proposals.d.ts +1 -0
- package/dist/operatorAgent/proposals.js +23 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/pipelinePreflight.js +1 -0
- package/dist/runstore/createStore.d.ts +2 -0
- package/dist/runstore/createStore.js +8 -0
- package/dist/runstore/overviewSummary.d.ts +56 -0
- package/dist/runstore/overviewSummary.js +134 -0
- package/dist/runstore/port.d.ts +3 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
- package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
- package/dist/runstore/sqlite/migrations/index.js +4 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
- package/dist/runtime/gitCheckoutCapability.js +1 -1
- package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
- package/dist/runtime/gitWorktreeCheckout.js +120 -0
- package/dist/runtime/sandboxContainer.d.ts +62 -0
- package/dist/runtime/sandboxContainer.js +151 -0
- package/dist/runtime/sandboxDesk.d.ts +28 -0
- package/dist/runtime/sandboxDesk.js +242 -0
- package/dist/runtime/sandboxFiles.d.ts +20 -0
- package/dist/runtime/sandboxFiles.js +150 -0
- package/dist/runtime/stageAttemptBootstrap.js +15 -1
- package/dist/runtime/stageEnvironment.d.ts +2 -0
- package/dist/runtime/stageEnvironment.js +8 -1
- package/dist/runtime/stageProcessLauncher.js +23 -0
- package/dist/runtime/stageRoots.d.ts +9 -0
- package/dist/runtime/stageRoots.js +14 -0
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +38 -4
- package/dist/runtime/stageSandbox.d.ts +16 -0
- package/dist/runtime/stageSandbox.js +54 -0
- package/dist/runtime/stageSecrets.d.ts +6 -1
- package/dist/runtime/stageSecrets.js +34 -1
- package/dist/runtime/stageWorker.js +138 -105
- package/dist/runtime/workspaceBinding.d.ts +2 -1
- package/dist/runtime/workspaceBinding.js +20 -4
- package/dist/server/agentAuthorRoutes.d.ts +5 -0
- package/dist/server/agentAuthorRoutes.js +64 -0
- package/dist/server/agentRoutes.d.ts +5 -0
- package/dist/server/agentRoutes.js +323 -0
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +36 -10
- package/dist/server/controlToken.js +8 -0
- package/dist/server/http.d.ts +8 -1
- package/dist/server/http.js +172 -20
- package/dist/server/mcpHost.js +1 -0
- package/dist/server/memoryRoutes.d.ts +5 -0
- package/dist/server/memoryRoutes.js +146 -0
- package/dist/server/projectMcpRoutes.d.ts +1 -0
- package/dist/server/projectMcpRoutes.js +241 -3
- package/dist/toolkit/guard.d.ts +27 -0
- package/dist/toolkit/guard.js +98 -0
- package/dist/toolkit/index.d.ts +48 -0
- package/dist/toolkit/index.js +82 -0
- package/dist/toolkit/meta.d.ts +36 -0
- package/dist/toolkit/meta.js +117 -0
- package/dist/toolkit/profiles.d.ts +13 -0
- package/dist/toolkit/profiles.js +25 -0
- package/dist/toolkit/record.d.ts +15 -0
- package/dist/toolkit/record.js +17 -0
- package/dist/tools/memoryTools.d.ts +95 -0
- package/dist/tools/memoryTools.js +179 -0
- package/dist/types/pipeline.d.ts +3 -0
- package/dist/types/stage.d.ts +2 -0
- package/dist/types/task.d.ts +11 -1
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
- package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
- package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
- package/dist/ui/assets/chat-BbLukq9W.js +5 -0
- package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
- package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
- package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
- package/dist/ui/assets/main-CijYMeM0.js +259 -0
- package/dist/ui/chat.html +25 -0
- package/dist/ui/index.html +6 -2
- package/dist/workshop/modelSettings.d.ts +2 -2
- package/dist/workshop/modelSettings.js +5 -11
- package/examples/README.md +1 -0
- package/examples/channels-smoke/README.md +12 -1
- package/examples/github-release/README.md +1 -1
- package/examples/memory/README.md +101 -0
- package/examples/memory/desk-outage.task.yaml +15 -0
- package/examples/memory/desk-refund.task.yaml +15 -0
- package/examples/memory/desk-revisit-outage.task.yaml +11 -0
- package/examples/memory/desk-revisit.pipeline.yaml +123 -0
- package/examples/memory/desk.pipeline.yaml +192 -0
- package/examples/memory/file-card.pipeline.yaml +39 -0
- package/examples/memory/file-ledger.task.yaml +22 -0
- package/examples/memory/file-release.task.yaml +24 -0
- package/examples/memory/file-rich.pipeline.yaml +40 -0
- package/examples/memory/file-ticket.task.yaml +7 -0
- package/examples/memory/file-timeline.task.yaml +28 -0
- package/examples/memory/find-card.pipeline.yaml +46 -0
- package/examples/memory/find-ticket.task.yaml +7 -0
- package/examples/memory/missing.pipeline.yaml +16 -0
- package/examples/memory/probe-bash.task.yaml +3 -0
- package/examples/memory/recall.pipeline.yaml +24 -0
- package/examples/memory/recall.task.yaml +5 -0
- package/examples/memory/remember-ada.task.yaml +6 -0
- package/examples/memory/remember-grace.task.yaml +6 -0
- package/examples/memory/remember.pipeline.yaml +58 -0
- package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
- package/examples/memory/stageflow.yaml +12 -0
- package/examples/memory/update-card.pipeline.yaml +43 -0
- package/examples/memory/update-ledger.task.yaml +23 -0
- package/examples/memory/update-rich.pipeline.yaml +44 -0
- package/examples/memory/update-ticket.task.yaml +7 -0
- package/examples/ship-feature/README.md +15 -0
- package/examples/stage-mcp/README.md +1 -1
- package/package.json +2 -1
- package/dist/ui/assets/index-BOhqnPNg.js +0 -260
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
import { StageMcpError, resolveStageMcpServers, } from "../config/resolveStageMcpServers.js";
|
|
2
|
+
import { collectSecretValues, scrubText } from "../config/mcpRedaction.js";
|
|
3
|
+
import { listCachedMcpTools } from "../config/mcpToolCache.js";
|
|
2
4
|
import * as piIsolatedMcp from "./piIsolatedMcp.js";
|
|
3
5
|
export const PROJECT_MCP_PROBE_TIMEOUT_MS = 30_000;
|
|
4
6
|
const MCP_STATUS_EVENT = "pi-mcp-adapter/status/v1";
|
|
5
7
|
function scrub(message, secrets) {
|
|
6
|
-
|
|
7
|
-
for (const secret of secrets) {
|
|
8
|
-
if (secret.length === 0)
|
|
9
|
-
continue;
|
|
10
|
-
out = out.split(secret).join("[redacted]");
|
|
11
|
-
}
|
|
12
|
-
return out;
|
|
13
|
-
}
|
|
14
|
-
function collectInterpolatedSecrets(entry) {
|
|
15
|
-
const secrets = [];
|
|
16
|
-
for (const field of ["env", "headers"]) {
|
|
17
|
-
const value = entry[field];
|
|
18
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
for (const item of Object.values(value)) {
|
|
22
|
-
if (typeof item === "string" && item.length > 0) {
|
|
23
|
-
secrets.push(item);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return secrets;
|
|
8
|
+
return scrubText(message, secrets);
|
|
28
9
|
}
|
|
29
10
|
function catalogErrorResult(name, err) {
|
|
30
11
|
if (!(err instanceof StageMcpError))
|
|
@@ -35,6 +16,9 @@ function catalogErrorResult(name, err) {
|
|
|
35
16
|
if (err.code === "missing_catalog") {
|
|
36
17
|
return { name, status: "missing_catalog" };
|
|
37
18
|
}
|
|
19
|
+
if (err.code === "host_state_corrupt") {
|
|
20
|
+
return { name, status: "invalid_config", error: err.message };
|
|
21
|
+
}
|
|
38
22
|
if (err.code === "invalid_config" ||
|
|
39
23
|
err.code === "reserved_name" ||
|
|
40
24
|
err.code === "unknown_server") {
|
|
@@ -136,7 +120,7 @@ function instantiateProbeFactories(attached, projectRoot) {
|
|
|
136
120
|
probeApi.emitSessionStart(projectRoot);
|
|
137
121
|
return () => probeApi.emitSessionShutdown();
|
|
138
122
|
}
|
|
139
|
-
function mapConnectingError(err, aborted) {
|
|
123
|
+
function mapConnectingError(err, aborted, secrets = []) {
|
|
140
124
|
const message = err instanceof Error ? err.message : String(err);
|
|
141
125
|
if (aborted || /cancelled/i.test(message)) {
|
|
142
126
|
return { kind: "cancelled" };
|
|
@@ -147,7 +131,7 @@ function mapConnectingError(err, aborted) {
|
|
|
147
131
|
return {
|
|
148
132
|
kind: "mapped",
|
|
149
133
|
status: "connect_failed",
|
|
150
|
-
error: message.length > 0 ? message : undefined,
|
|
134
|
+
error: message.length > 0 ? scrubText(message, secrets) : undefined,
|
|
151
135
|
};
|
|
152
136
|
}
|
|
153
137
|
async function waitForProbeStatus(eventBus, serverName, connecting, options) {
|
|
@@ -193,7 +177,7 @@ async function waitForProbeStatus(eventBus, serverName, connecting, options) {
|
|
|
193
177
|
const maybeUnsub = eventBus.on(MCP_STATUS_EVENT, handler);
|
|
194
178
|
unsubscribe = typeof maybeUnsub === "function" ? maybeUnsub : undefined;
|
|
195
179
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
196
|
-
void (connecting ?? Promise.resolve()).then(() => finish({ kind: "mapped", status: "connected" }), (err) => finish(mapConnectingError(err, options.signal?.aborted === true)));
|
|
180
|
+
void (connecting ?? Promise.resolve()).then(() => finish({ kind: "mapped", status: "connected" }), (err) => finish(mapConnectingError(err, options.signal?.aborted === true, options.secrets)));
|
|
197
181
|
timer = setTimeout(() => {
|
|
198
182
|
finish({
|
|
199
183
|
kind: "mapped",
|
|
@@ -206,7 +190,6 @@ async function waitForProbeStatus(eventBus, serverName, connecting, options) {
|
|
|
206
190
|
export async function probeProjectMcpServer(options) {
|
|
207
191
|
const name = options.name;
|
|
208
192
|
const timeoutMs = options.timeoutMs ?? PROJECT_MCP_PROBE_TIMEOUT_MS;
|
|
209
|
-
const env = options.env ?? process.env;
|
|
210
193
|
if (options.signal?.aborted) {
|
|
211
194
|
return { name, status: "cancelled" };
|
|
212
195
|
}
|
|
@@ -215,7 +198,9 @@ export async function probeProjectMcpServer(options) {
|
|
|
215
198
|
snapshot = await resolveStageMcpServers({
|
|
216
199
|
projectRoot: options.projectRoot,
|
|
217
200
|
allowlist: [name],
|
|
218
|
-
env,
|
|
201
|
+
env: {},
|
|
202
|
+
hostEnv: options.hostEnv ?? process.env,
|
|
203
|
+
scope: { kind: "probe" },
|
|
219
204
|
});
|
|
220
205
|
}
|
|
221
206
|
catch (err) {
|
|
@@ -228,7 +213,7 @@ export async function probeProjectMcpServer(options) {
|
|
|
228
213
|
if (resolved === undefined) {
|
|
229
214
|
return { name, status: "invalid_config" };
|
|
230
215
|
}
|
|
231
|
-
const secrets =
|
|
216
|
+
const secrets = collectSecretValues(resolved);
|
|
232
217
|
let attached;
|
|
233
218
|
let shutdownFactories;
|
|
234
219
|
const dispose = async () => {
|
|
@@ -254,7 +239,7 @@ export async function probeProjectMcpServer(options) {
|
|
|
254
239
|
return { name, status: "invalid_config" };
|
|
255
240
|
}
|
|
256
241
|
shutdownFactories = instantiateProbeFactories(attached, options.projectRoot);
|
|
257
|
-
const waited = await waitForProbeStatus(attached.eventBus, name, attached.connecting, { timeoutMs, signal: options.signal });
|
|
242
|
+
const waited = await waitForProbeStatus(attached.eventBus, name, attached.connecting, { timeoutMs, signal: options.signal, secrets });
|
|
258
243
|
if (waited.kind === "cancelled") {
|
|
259
244
|
return { name, status: "cancelled" };
|
|
260
245
|
}
|
|
@@ -267,7 +252,8 @@ export async function probeProjectMcpServer(options) {
|
|
|
267
252
|
: {}),
|
|
268
253
|
};
|
|
269
254
|
}
|
|
270
|
-
|
|
255
|
+
const tools = waited.status === "connected" ? listCachedMcpTools(name)?.tools : undefined;
|
|
256
|
+
return { name, status: waited.status, ...(tools ? { tools } : {}) };
|
|
271
257
|
}
|
|
272
258
|
catch (err) {
|
|
273
259
|
if (options.signal?.aborted) {
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
|
|
|
9
9
|
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
10
10
|
import type { StageEmail } from "../email/host.js";
|
|
11
11
|
import type { StageChannels } from "../channels/host.js";
|
|
12
|
+
import type { StageMemory } from "../memory/access.js";
|
|
12
13
|
import type { QaExchange } from "../hitl/qaTrail.js";
|
|
13
14
|
import type { StageUsage } from "../types/usage.js";
|
|
14
15
|
/** Opaque to runtime; adapters interpret. */
|
|
@@ -67,6 +68,10 @@ export type StageRunInput = {
|
|
|
67
68
|
skillFilePath?: string;
|
|
68
69
|
email?: StageEmail;
|
|
69
70
|
channels?: StageChannels;
|
|
71
|
+
/** Memories attached to this stage. The host reads and replaces the page. */
|
|
72
|
+
memory?: StageMemory;
|
|
73
|
+
/** Refreshed memory prompt; preferred over deriving from `memory` at prompt build time. */
|
|
74
|
+
memoryPrompt?: string;
|
|
70
75
|
/** Bundled browser skill; set by the runtime when the stage has `browser`. */
|
|
71
76
|
browserSkillFilePath?: string;
|
|
72
77
|
forkEmitContext?: ForkEmitContext;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { type SandboxFileTarget } from "../runtime/sandboxFiles.js";
|
|
3
|
+
export type SandboxPiToolOptions = SandboxFileTarget;
|
|
4
|
+
export declare function createSandboxReadTool(options: SandboxPiToolOptions): {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description: string;
|
|
8
|
+
promptSnippet: string;
|
|
9
|
+
promptGuidelines: string[];
|
|
10
|
+
parameters: Type.TObject<{
|
|
11
|
+
path: Type.TString;
|
|
12
|
+
offset: Type.TOptional<Type.TNumber>;
|
|
13
|
+
limit: Type.TOptional<Type.TNumber>;
|
|
14
|
+
}>;
|
|
15
|
+
execute: (_toolCallId: string, params: {
|
|
16
|
+
path: string;
|
|
17
|
+
offset?: number;
|
|
18
|
+
limit?: number;
|
|
19
|
+
}) => Promise<{
|
|
20
|
+
content: {
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
details: undefined;
|
|
25
|
+
isError?: boolean | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
export declare function createSandboxWriteTool(options: SandboxPiToolOptions): {
|
|
29
|
+
name: string;
|
|
30
|
+
label: string;
|
|
31
|
+
description: string;
|
|
32
|
+
promptSnippet: string;
|
|
33
|
+
promptGuidelines: string[];
|
|
34
|
+
parameters: Type.TObject<{
|
|
35
|
+
path: Type.TString;
|
|
36
|
+
content: Type.TString;
|
|
37
|
+
}>;
|
|
38
|
+
execute: (_toolCallId: string, params: {
|
|
39
|
+
path: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}) => Promise<{
|
|
42
|
+
content: {
|
|
43
|
+
type: "text";
|
|
44
|
+
text: string;
|
|
45
|
+
}[];
|
|
46
|
+
details: undefined;
|
|
47
|
+
isError?: boolean | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
export declare function createSandboxEditTool(options: SandboxPiToolOptions): {
|
|
51
|
+
name: string;
|
|
52
|
+
label: string;
|
|
53
|
+
description: string;
|
|
54
|
+
promptSnippet: string;
|
|
55
|
+
promptGuidelines: string[];
|
|
56
|
+
parameters: Type.TObject<{
|
|
57
|
+
path: Type.TString;
|
|
58
|
+
edits: Type.TArray<Type.TObject<{
|
|
59
|
+
oldText: Type.TString;
|
|
60
|
+
newText: Type.TString;
|
|
61
|
+
}>>;
|
|
62
|
+
}>;
|
|
63
|
+
execute: (_toolCallId: string, params: {
|
|
64
|
+
path: string;
|
|
65
|
+
edits: Array<{
|
|
66
|
+
oldText: string;
|
|
67
|
+
newText: string;
|
|
68
|
+
}>;
|
|
69
|
+
}) => Promise<{
|
|
70
|
+
content: {
|
|
71
|
+
type: "text";
|
|
72
|
+
text: string;
|
|
73
|
+
}[];
|
|
74
|
+
details: undefined;
|
|
75
|
+
isError?: boolean | undefined;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
export declare function createSandboxBashTool(options: SandboxPiToolOptions): {
|
|
79
|
+
name: string;
|
|
80
|
+
label: string;
|
|
81
|
+
description: string;
|
|
82
|
+
promptSnippet: string;
|
|
83
|
+
promptGuidelines: string[];
|
|
84
|
+
parameters: Type.TObject<{
|
|
85
|
+
command: Type.TString;
|
|
86
|
+
timeout: Type.TOptional<Type.TNumber>;
|
|
87
|
+
}>;
|
|
88
|
+
execute: (_toolCallId: string, params: {
|
|
89
|
+
command: string;
|
|
90
|
+
timeout?: number;
|
|
91
|
+
}) => Promise<{
|
|
92
|
+
content: {
|
|
93
|
+
type: "text";
|
|
94
|
+
text: string;
|
|
95
|
+
}[];
|
|
96
|
+
details: undefined;
|
|
97
|
+
isError?: boolean | undefined;
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { editSandboxFile, readSandboxFile, runSandboxBash, sliceSandboxLines, writeSandboxFile, } from "../runtime/sandboxFiles.js";
|
|
3
|
+
function textResult(text, isError = false) {
|
|
4
|
+
return {
|
|
5
|
+
content: [{ type: "text", text }],
|
|
6
|
+
details: undefined,
|
|
7
|
+
...(isError ? { isError: true } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function failure(err) {
|
|
11
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12
|
+
return textResult(message, true);
|
|
13
|
+
}
|
|
14
|
+
export function createSandboxReadTool(options) {
|
|
15
|
+
return {
|
|
16
|
+
name: "read",
|
|
17
|
+
label: "read",
|
|
18
|
+
description: "Read a file inside this stage's sandbox. Paths are relative to /workspace (artifacts/, checkout/, grants/, work/) or absolute under /workspace. Host paths are rejected.",
|
|
19
|
+
promptSnippet: "Read file contents inside /workspace",
|
|
20
|
+
promptGuidelines: [
|
|
21
|
+
"Use read to examine files under /workspace instead of cat or sed.",
|
|
22
|
+
],
|
|
23
|
+
parameters: Type.Object({
|
|
24
|
+
path: Type.String({ description: "Path to the file to read, relative to /workspace or absolute under /workspace" }),
|
|
25
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
26
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
27
|
+
}),
|
|
28
|
+
execute: async (_toolCallId, params) => {
|
|
29
|
+
try {
|
|
30
|
+
const text = await readSandboxFile(options, params.path);
|
|
31
|
+
return textResult(sliceSandboxLines(text, params.offset, params.limit));
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return failure(err);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function createSandboxWriteTool(options) {
|
|
40
|
+
return {
|
|
41
|
+
name: "write",
|
|
42
|
+
label: "write",
|
|
43
|
+
description: "Write a file inside this stage's sandbox. Paths are relative to /workspace or absolute under /workspace. Parent directories are created. Host paths are rejected.",
|
|
44
|
+
promptSnippet: "Create or overwrite files inside /workspace",
|
|
45
|
+
promptGuidelines: ["Use write only for new files or complete rewrites under /workspace."],
|
|
46
|
+
parameters: Type.Object({
|
|
47
|
+
path: Type.String({ description: "Path to the file to write, relative to /workspace or absolute under /workspace" }),
|
|
48
|
+
content: Type.String({ description: "Content to write to the file" }),
|
|
49
|
+
}),
|
|
50
|
+
execute: async (_toolCallId, params) => {
|
|
51
|
+
try {
|
|
52
|
+
await writeSandboxFile(options, params.path, params.content);
|
|
53
|
+
return textResult(`Successfully wrote to ${params.path}`);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return failure(err);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function createSandboxEditTool(options) {
|
|
62
|
+
return {
|
|
63
|
+
name: "edit",
|
|
64
|
+
label: "edit",
|
|
65
|
+
description: "Edit a file inside this stage's sandbox with exact text replacement. Paths are relative to /workspace or absolute under /workspace. Each oldText must match exactly once in the original file.",
|
|
66
|
+
promptSnippet: "Make precise file edits inside /workspace",
|
|
67
|
+
promptGuidelines: [
|
|
68
|
+
"Use edit for precise changes under /workspace (edits[].oldText must match exactly once).",
|
|
69
|
+
],
|
|
70
|
+
parameters: Type.Object({
|
|
71
|
+
path: Type.String({ description: "Path to the file to edit, relative to /workspace or absolute under /workspace" }),
|
|
72
|
+
edits: Type.Array(Type.Object({
|
|
73
|
+
oldText: Type.String({ description: "Exact text to replace. It must be unique in the original file." }),
|
|
74
|
+
newText: Type.String({ description: "Replacement text." }),
|
|
75
|
+
}), { description: "One or more targeted replacements against the original file." }),
|
|
76
|
+
}),
|
|
77
|
+
execute: async (_toolCallId, params) => {
|
|
78
|
+
try {
|
|
79
|
+
await editSandboxFile(options, params.path, params.edits);
|
|
80
|
+
return textResult(`Successfully edited ${params.path}`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return failure(err);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function createSandboxBashTool(options) {
|
|
89
|
+
return {
|
|
90
|
+
name: "bash",
|
|
91
|
+
label: "bash",
|
|
92
|
+
description: "Execute a bash command in this stage's sandbox. The working directory is /workspace. The host filesystem is not mounted.",
|
|
93
|
+
promptSnippet: "Execute bash commands in /workspace",
|
|
94
|
+
promptGuidelines: ["The shell starts in /workspace. Do not use host paths."],
|
|
95
|
+
parameters: Type.Object({
|
|
96
|
+
command: Type.String({ description: "Shell command to execute" }),
|
|
97
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
|
|
98
|
+
}),
|
|
99
|
+
execute: async (_toolCallId, params) => {
|
|
100
|
+
try {
|
|
101
|
+
const text = await runSandboxBash(options, params.command, params.timeout !== undefined ? params.timeout * 1000 : undefined);
|
|
102
|
+
return textResult(text);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return failure(err);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** What an agent id looks like. Lives here, not in the loader, so the loader can import the reserved list without a cycle. */
|
|
2
|
+
export declare const AGENT_ID_PATTERN: RegExp;
|
|
3
|
+
/** Ids that collide with fixed `/api/agents/<word>` routes. The single list: the loader and the generator both read it. */
|
|
4
|
+
export declare const RESERVED_AGENT_IDS: ReadonlySet<string>;
|
|
5
|
+
export declare function isReservedAgentId(id: string): boolean;
|
|
6
|
+
export declare function slugAgentId(name: string): string;
|
|
7
|
+
/** An id from the agent's name, made unique with a numeric suffix. Names with no usable letters become `agent`. */
|
|
8
|
+
export declare function generateAgentId(name: string, isTaken: (id: string) => Promise<boolean>): Promise<string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** What an agent id looks like. Lives here, not in the loader, so the loader can import the reserved list without a cycle. */
|
|
2
|
+
export const AGENT_ID_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
3
|
+
/** Ids that collide with fixed `/api/agents/<word>` routes. The single list: the loader and the generator both read it. */
|
|
4
|
+
export const RESERVED_AGENT_IDS = new Set(["sessions", "approvals", "new"]);
|
|
5
|
+
export function isReservedAgentId(id) {
|
|
6
|
+
return RESERVED_AGENT_IDS.has(id);
|
|
7
|
+
}
|
|
8
|
+
export function slugAgentId(name) {
|
|
9
|
+
const slug = name
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
13
|
+
.replace(/^_+|_+$/g, "")
|
|
14
|
+
.replace(/^[^a-z]+/, "")
|
|
15
|
+
.slice(0, 56);
|
|
16
|
+
return AGENT_ID_PATTERN.test(slug) ? slug : "";
|
|
17
|
+
}
|
|
18
|
+
/** An id from the agent's name, made unique with a numeric suffix. Names with no usable letters become `agent`. */
|
|
19
|
+
export async function generateAgentId(name, isTaken) {
|
|
20
|
+
const base = slugAgentId(name) || "agent";
|
|
21
|
+
for (let attempt = 1; attempt < 1000; attempt += 1) {
|
|
22
|
+
const candidate = attempt === 1 ? base : `${base}_${attempt}`;
|
|
23
|
+
if (!RESERVED_AGENT_IDS.has(candidate) && !(await isTaken(candidate)))
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Could not generate a unique agent id");
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ApprovalOutcome = "approved" | "denied" | "expired";
|
|
2
|
+
/**
|
|
3
|
+
* In-process rendezvous between a turn paused at an ask-first tool and the
|
|
4
|
+
* decision that arrives later (console, CLI, MCP or a channel). A waiter only
|
|
5
|
+
* exists while the Host process that started the turn is alive.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ApprovalBroker {
|
|
8
|
+
private readonly waiters;
|
|
9
|
+
wait(approvalId: string, options: {
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}): Promise<ApprovalOutcome | "aborted">;
|
|
14
|
+
settle(approvalId: string, outcome: ApprovalOutcome): void;
|
|
15
|
+
pending(): number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process rendezvous between a turn paused at an ask-first tool and the
|
|
3
|
+
* decision that arrives later (console, CLI, MCP or a channel). A waiter only
|
|
4
|
+
* exists while the Host process that started the turn is alive.
|
|
5
|
+
*/
|
|
6
|
+
export class ApprovalBroker {
|
|
7
|
+
waiters = new Map();
|
|
8
|
+
wait(approvalId, options) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
let timer;
|
|
11
|
+
const finish = (outcome) => {
|
|
12
|
+
if (timer)
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
15
|
+
this.waiters.get(approvalId)?.delete(listener);
|
|
16
|
+
if (this.waiters.get(approvalId)?.size === 0)
|
|
17
|
+
this.waiters.delete(approvalId);
|
|
18
|
+
resolve(outcome);
|
|
19
|
+
};
|
|
20
|
+
const listener = (outcome) => finish(outcome);
|
|
21
|
+
const onAbort = () => finish("aborted");
|
|
22
|
+
if (options.signal.aborted)
|
|
23
|
+
return resolve("aborted");
|
|
24
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
const set = this.waiters.get(approvalId) ?? new Set();
|
|
26
|
+
set.add(listener);
|
|
27
|
+
this.waiters.set(approvalId, set);
|
|
28
|
+
const arm = () => {
|
|
29
|
+
const remaining = Date.parse(options.expiresAt) - (options.now?.() ?? Date.now());
|
|
30
|
+
if (remaining <= 0)
|
|
31
|
+
return finish("expired");
|
|
32
|
+
// setTimeout caps at ~24.8 days; longer waits re-arm until the real expiry.
|
|
33
|
+
timer = setTimeout(arm, Math.min(remaining, 2 ** 31 - 1));
|
|
34
|
+
};
|
|
35
|
+
timer = setTimeout(arm, 0);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
settle(approvalId, outcome) {
|
|
39
|
+
for (const listener of [...(this.waiters.get(approvalId) ?? [])])
|
|
40
|
+
listener(outcome);
|
|
41
|
+
}
|
|
42
|
+
pending() {
|
|
43
|
+
return this.waiters.size;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type SummaryLine } from "./definitionSummary.js";
|
|
2
|
+
import { type AgentAuthorDraft, type OperatorAgentHost } from "../operatorAgent/index.js";
|
|
3
|
+
export type AgentAuthorTurnResult = {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
text: string;
|
|
6
|
+
draft: AgentAuthorDraft;
|
|
7
|
+
/** One line per config section this turn changed. Empty when the turn only talked. */
|
|
8
|
+
summaryLines: SummaryLine[];
|
|
9
|
+
};
|
|
10
|
+
export declare function resetAgentAuthorSessionsForTests(): void;
|
|
11
|
+
export declare function runAgentAuthorTurn(input: {
|
|
12
|
+
host: OperatorAgentHost;
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
message: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
draft?: AgentAuthorDraft;
|
|
17
|
+
}): Promise<AgentAuthorTurnResult>;
|
|
18
|
+
export declare function undoAgentAuthorTurn(sessionId: string): AgentAuthorDraft;
|
|
19
|
+
export declare function parseAgentAuthorDraft(body: unknown): AgentAuthorDraft;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { describeChanges } from "./definitionSummary.js";
|
|
3
|
+
import { AGENT_AUTHOR_PROFILE_ID, createAgentAuthorContext, isAgentAuthorContext, readAgentAuthorDraft, } from "../operatorAgent/index.js";
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
export function resetAgentAuthorSessionsForTests() {
|
|
6
|
+
for (const session of sessions.values())
|
|
7
|
+
session.close();
|
|
8
|
+
sessions.clear();
|
|
9
|
+
}
|
|
10
|
+
export async function runAgentAuthorTurn(input) {
|
|
11
|
+
const message = input.message.trim();
|
|
12
|
+
if (!message)
|
|
13
|
+
throw new Error("agent.invalid");
|
|
14
|
+
let sessionId = input.sessionId?.trim();
|
|
15
|
+
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
16
|
+
if (sessionId && !session)
|
|
17
|
+
throw new Error("agent.session_not_found");
|
|
18
|
+
const posted = createAgentAuthorContext({
|
|
19
|
+
projectRoot: input.projectRoot,
|
|
20
|
+
draft: input.draft,
|
|
21
|
+
});
|
|
22
|
+
if (!session) {
|
|
23
|
+
sessionId = randomUUID();
|
|
24
|
+
session = input.host.openSession({
|
|
25
|
+
profileId: AGENT_AUTHOR_PROFILE_ID,
|
|
26
|
+
context: posted,
|
|
27
|
+
});
|
|
28
|
+
sessions.set(sessionId, session);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
session.setContext(posted);
|
|
32
|
+
}
|
|
33
|
+
const before = readAgentAuthorDraft(session.getContext());
|
|
34
|
+
const events = await session.send(message);
|
|
35
|
+
const text = events
|
|
36
|
+
.filter((event) => event.type === "message" && event.role === "assistant")
|
|
37
|
+
.map((event) => (event.type === "message" ? event.text : ""))
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join("\n\n");
|
|
40
|
+
const error = events.find((event) => event.type === "error");
|
|
41
|
+
const draft = readAgentAuthorDraft(session.getContext());
|
|
42
|
+
return {
|
|
43
|
+
sessionId: sessionId,
|
|
44
|
+
text: text || (error && error.type === "error" ? error.message : ""),
|
|
45
|
+
draft,
|
|
46
|
+
summaryLines: describeChanges(before, draft),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function undoAgentAuthorTurn(sessionId) {
|
|
50
|
+
const session = sessions.get(sessionId);
|
|
51
|
+
if (!session)
|
|
52
|
+
throw new Error("agent.session_not_found");
|
|
53
|
+
const undone = session.undoMutation();
|
|
54
|
+
if (!undone.ok)
|
|
55
|
+
throw new Error(undone.reason === "conflict" ? "agent.conflict" : "agent.invalid");
|
|
56
|
+
const context = session.getContext();
|
|
57
|
+
if (!isAgentAuthorContext(context))
|
|
58
|
+
throw new Error("agent.invalid");
|
|
59
|
+
return readAgentAuthorDraft(context);
|
|
60
|
+
}
|
|
61
|
+
export function parseAgentAuthorDraft(body) {
|
|
62
|
+
return body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentSessionDetail } from "./service.js";
|
|
2
|
+
import { type ConversationOptions, type SendTurnInput } from "./sessions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Serializes turns per session (or per agent while a session is being
|
|
5
|
+
* opened) and keeps a handle on the running turn so it can be stopped.
|
|
6
|
+
*/
|
|
7
|
+
export declare class AgentChatController {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private readonly turnChains;
|
|
10
|
+
private readonly activeBySession;
|
|
11
|
+
private readonly activeByAgent;
|
|
12
|
+
/** Sessions whose turn was stopped: their runtime session is released once the turn has wound down. */
|
|
13
|
+
private readonly stopped;
|
|
14
|
+
constructor(options: ConversationOptions);
|
|
15
|
+
private runExclusive;
|
|
16
|
+
send(input: SendTurnInput): Promise<AgentSessionDetail>;
|
|
17
|
+
/** Stop the running turn on a session. Returns false when nothing was running. */
|
|
18
|
+
stop(sessionId: string): boolean;
|
|
19
|
+
/** Let the runtime drop what it holds for a session (model session, isolated MCP processes). Never throws. */
|
|
20
|
+
release(sessionId: string): Promise<void>;
|
|
21
|
+
/** Stop a turn that is still opening its session (client left before the session id arrived). */
|
|
22
|
+
stopOpening(agentId: string): boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AgentConversation } from "./sessions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serializes turns per session (or per agent while a session is being
|
|
4
|
+
* opened) and keeps a handle on the running turn so it can be stopped.
|
|
5
|
+
*/
|
|
6
|
+
export class AgentChatController {
|
|
7
|
+
options;
|
|
8
|
+
turnChains = new Map();
|
|
9
|
+
activeBySession = new Map();
|
|
10
|
+
activeByAgent = new Map();
|
|
11
|
+
/** Sessions whose turn was stopped: their runtime session is released once the turn has wound down. */
|
|
12
|
+
stopped = new Set();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
runExclusive(key, work) {
|
|
17
|
+
const previous = this.turnChains.get(key) ?? Promise.resolve();
|
|
18
|
+
const run = previous.then(work, work);
|
|
19
|
+
this.turnChains.set(key, run.then(() => undefined, () => undefined));
|
|
20
|
+
return run;
|
|
21
|
+
}
|
|
22
|
+
send(input) {
|
|
23
|
+
return this.runExclusive(input.sessionId ?? `agent:${input.agentId}`, async () => {
|
|
24
|
+
const chat = new AgentConversation(this.options);
|
|
25
|
+
if (input.sessionId)
|
|
26
|
+
this.activeBySession.set(input.sessionId, chat);
|
|
27
|
+
else
|
|
28
|
+
this.activeByAgent.set(input.agentId, chat);
|
|
29
|
+
let openedSessionId;
|
|
30
|
+
try {
|
|
31
|
+
return await chat.send({
|
|
32
|
+
...input,
|
|
33
|
+
onSessionOpened: (sessionId) => {
|
|
34
|
+
openedSessionId = sessionId;
|
|
35
|
+
this.activeBySession.set(sessionId, chat);
|
|
36
|
+
input.onSessionOpened?.(sessionId);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
const sessionId = input.sessionId ?? openedSessionId;
|
|
42
|
+
if (sessionId && this.stopped.delete(sessionId))
|
|
43
|
+
await this.release(sessionId);
|
|
44
|
+
if (sessionId && this.activeBySession.get(sessionId) === chat)
|
|
45
|
+
this.activeBySession.delete(sessionId);
|
|
46
|
+
if (this.activeByAgent.get(input.agentId) === chat)
|
|
47
|
+
this.activeByAgent.delete(input.agentId);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Stop the running turn on a session. Returns false when nothing was running. */
|
|
52
|
+
stop(sessionId) {
|
|
53
|
+
const chat = this.activeBySession.get(sessionId);
|
|
54
|
+
if (!chat)
|
|
55
|
+
return false;
|
|
56
|
+
this.stopped.add(sessionId);
|
|
57
|
+
chat.stop();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** Let the runtime drop what it holds for a session (model session, isolated MCP processes). Never throws. */
|
|
61
|
+
async release(sessionId) {
|
|
62
|
+
try {
|
|
63
|
+
await this.options.backend.release?.(sessionId);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.options.logError?.(`releasing agent session ${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Stop a turn that is still opening its session (client left before the session id arrived). */
|
|
70
|
+
stopOpening(agentId) {
|
|
71
|
+
const chat = this.activeByAgent.get(agentId);
|
|
72
|
+
if (!chat)
|
|
73
|
+
return false;
|
|
74
|
+
chat.stop();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentInboundMessage } from "./port.js";
|
|
2
|
+
export type AgentMailInboundConfig = {
|
|
3
|
+
connectionId: string;
|
|
4
|
+
webhookSecret: string;
|
|
5
|
+
};
|
|
6
|
+
export type AgentMailParseResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
message: AgentInboundMessage;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
code: "agent.signature_invalid" | "agent.invalid";
|
|
12
|
+
};
|
|
13
|
+
export declare function verifyAgentMailSignature(config: AgentMailInboundConfig, signatureHeader: string | undefined, rawBody: string): {
|
|
14
|
+
ok: true;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
code: "agent.signature_invalid";
|
|
18
|
+
};
|
|
19
|
+
export declare function parseAgentMailInbound(config: AgentMailInboundConfig, payload: unknown): AgentMailParseResult;
|