@mcoda/mswarm 0.1.134 → 0.1.135-codali.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/codali-executor.d.ts +63 -0
- package/dist/codali-executor.d.ts.map +1 -1
- package/dist/codali-executor.js +78 -8
- package/dist/codali-executor.js.map +1 -1
- package/dist/invocation-token.d.ts.map +1 -1
- package/dist/invocation-token.js +1 -0
- package/dist/invocation-token.js.map +1 -1
- package/dist/native-reasoning.d.ts +8 -0
- package/dist/native-reasoning.d.ts.map +1 -0
- package/dist/native-reasoning.js +18 -0
- package/dist/native-reasoning.js.map +1 -0
- package/dist/runtime.d.ts +23 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +181 -15
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -8
- package/dist/server.js.map +1 -1
- package/dist/vendor/codali/agents/RoleResolution.d.ts.map +1 -1
- package/dist/vendor/codali/agents/RoleResolution.js +4 -4
- package/dist/vendor/codali/api/CodaliApi.d.ts +16 -0
- package/dist/vendor/codali/api/CodaliApi.d.ts.map +1 -1
- package/dist/vendor/codali/api/CodaliApi.js +39 -10
- package/dist/vendor/codali/cli/AskCommand.d.ts +3 -0
- package/dist/vendor/codali/cli/AskCommand.d.ts.map +1 -1
- package/dist/vendor/codali/cli/AskCommand.js +76 -13
- package/dist/vendor/codali/cli/RunCommand.d.ts +4 -0
- package/dist/vendor/codali/cli/RunCommand.d.ts.map +1 -1
- package/dist/vendor/codali/cli/RunCommand.js +110 -24
- package/dist/vendor/codali/cli/TerminalCommand.d.ts +35 -0
- package/dist/vendor/codali/cli/TerminalCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/TerminalCommand.js +616 -0
- package/dist/vendor/codali/cli/TerminalCredentials.d.ts +3 -0
- package/dist/vendor/codali/cli/TerminalCredentials.d.ts.map +1 -0
- package/dist/vendor/codali/cli/TerminalCredentials.js +89 -0
- package/dist/vendor/codali/cli/TerminalInput.d.ts +24 -0
- package/dist/vendor/codali/cli/TerminalInput.d.ts.map +1 -0
- package/dist/vendor/codali/cli/TerminalInput.js +98 -0
- package/dist/vendor/codali/cli/TerminalText.d.ts +10 -0
- package/dist/vendor/codali/cli/TerminalText.d.ts.map +1 -0
- package/dist/vendor/codali/cli/TerminalText.js +53 -0
- package/dist/vendor/codali/cli.d.ts.map +1 -1
- package/dist/vendor/codali/cli.js +33 -4
- package/dist/vendor/codali/cognitive/ConfiguredPipeline.d.ts +34 -0
- package/dist/vendor/codali/cognitive/ConfiguredPipeline.d.ts.map +1 -0
- package/dist/vendor/codali/cognitive/ConfiguredPipeline.js +50 -0
- package/dist/vendor/codali/cognitive/ValidationRunner.d.ts +4 -0
- package/dist/vendor/codali/cognitive/ValidationRunner.d.ts.map +1 -1
- package/dist/vendor/codali/cognitive/ValidationRunner.js +18 -41
- package/dist/vendor/codali/config/NodeOrchestration.d.ts +21 -0
- package/dist/vendor/codali/config/NodeOrchestration.d.ts.map +1 -0
- package/dist/vendor/codali/config/NodeOrchestration.js +54 -0
- package/dist/vendor/codali/config/OrchestrationConfig.d.ts +100 -0
- package/dist/vendor/codali/config/OrchestrationConfig.d.ts.map +1 -0
- package/dist/vendor/codali/config/OrchestrationConfig.js +245 -0
- package/dist/vendor/codali/connectors/mcp/McpClient.d.ts +3 -1
- package/dist/vendor/codali/connectors/mcp/McpClient.d.ts.map +1 -1
- package/dist/vendor/codali/connectors/mcp/McpClient.js +3 -2
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts +2 -2
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts.map +1 -1
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.js +39 -16
- package/dist/vendor/codali/connectors/mcp/McpToolAdapter.js +2 -2
- package/dist/vendor/codali/connectors/media/ConfiguredMediaTools.d.ts +6 -0
- package/dist/vendor/codali/connectors/media/ConfiguredMediaTools.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/ConfiguredMediaTools.js +41 -0
- package/dist/vendor/codali/connectors/media/ImageGenerationTool.js +3 -3
- package/dist/vendor/codali/connectors/media/MediaArtifactReceipt.d.ts +5 -0
- package/dist/vendor/codali/connectors/media/MediaArtifactReceipt.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/MediaArtifactReceipt.js +65 -0
- package/dist/vendor/codali/connectors/media/MediaJobStore.d.ts +68 -0
- package/dist/vendor/codali/connectors/media/MediaJobStore.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/MediaJobStore.js +199 -0
- package/dist/vendor/codali/connectors/media/MediaJobWorker.d.ts +2 -0
- package/dist/vendor/codali/connectors/media/MediaJobWorker.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/MediaJobWorker.js +65 -0
- package/dist/vendor/codali/connectors/media/RemoteMediaJob.d.ts +6 -0
- package/dist/vendor/codali/connectors/media/RemoteMediaJob.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/RemoteMediaJob.js +80 -0
- package/dist/vendor/codali/docdex/DocdexClient.d.ts +18 -1
- package/dist/vendor/codali/docdex/DocdexClient.d.ts.map +1 -1
- package/dist/vendor/codali/docdex/DocdexClient.js +135 -19
- package/dist/vendor/codali/docdex/ManagedDocdex.d.ts +29 -0
- package/dist/vendor/codali/docdex/ManagedDocdex.d.ts.map +1 -0
- package/dist/vendor/codali/docdex/ManagedDocdex.js +215 -0
- package/dist/vendor/codali/docdex/ManagedRuntime.d.ts +28 -0
- package/dist/vendor/codali/docdex/ManagedRuntime.d.ts.map +1 -0
- package/dist/vendor/codali/docdex/ManagedRuntime.js +212 -0
- package/dist/vendor/codali/eval/EvaluationProcess.d.ts +13 -0
- package/dist/vendor/codali/eval/EvaluationProcess.d.ts.map +1 -0
- package/dist/vendor/codali/eval/EvaluationProcess.js +76 -0
- package/dist/vendor/codali/eval/LongWorkflowJourney.d.ts +34 -0
- package/dist/vendor/codali/eval/LongWorkflowJourney.d.ts.map +1 -0
- package/dist/vendor/codali/eval/LongWorkflowJourney.js +243 -0
- package/dist/vendor/codali/eval/PlanningCorpus.d.ts +45 -0
- package/dist/vendor/codali/eval/PlanningCorpus.d.ts.map +1 -0
- package/dist/vendor/codali/eval/PlanningCorpus.js +189 -0
- package/dist/vendor/codali/eval/PlanningCorpusRunner.d.ts +13 -0
- package/dist/vendor/codali/eval/PlanningCorpusRunner.d.ts.map +1 -0
- package/dist/vendor/codali/eval/PlanningCorpusRunner.js +89 -0
- package/dist/vendor/codali/eval/TerminalCorpusRunner.d.ts +36 -0
- package/dist/vendor/codali/eval/TerminalCorpusRunner.d.ts.map +1 -0
- package/dist/vendor/codali/eval/TerminalCorpusRunner.js +222 -0
- package/dist/vendor/codali/eval/TerminalTaskCorpus.d.ts +26 -0
- package/dist/vendor/codali/eval/TerminalTaskCorpus.d.ts.map +1 -0
- package/dist/vendor/codali/eval/TerminalTaskCorpus.js +105 -0
- package/dist/vendor/codali/eval/TerminalWorkflowCorpus.d.ts +11 -0
- package/dist/vendor/codali/eval/TerminalWorkflowCorpus.d.ts.map +1 -0
- package/dist/vendor/codali/eval/TerminalWorkflowCorpus.js +116 -0
- package/dist/vendor/codali/eval/fixtures/TerminalRecoveryJourney.d.ts +4 -0
- package/dist/vendor/codali/eval/fixtures/TerminalRecoveryJourney.d.ts.map +1 -0
- package/dist/vendor/codali/eval/fixtures/TerminalRecoveryJourney.js +132 -0
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts +2 -0
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/CodaliGateway.js +11 -1
- package/dist/vendor/codali/gateway/ConfiguredGateway.d.ts +23 -0
- package/dist/vendor/codali/gateway/ConfiguredGateway.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/ConfiguredGateway.js +44 -0
- package/dist/vendor/codali/gateway/GatewayTracer.d.ts +1 -1
- package/dist/vendor/codali/gateway/GatewayTracer.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/GatewayTracer.js +2 -27
- package/dist/vendor/codali/index.d.ts +23 -0
- package/dist/vendor/codali/index.d.ts.map +1 -1
- package/dist/vendor/codali/index.js +12 -0
- package/dist/vendor/codali/permissions/PermissionGate.d.ts +44 -0
- package/dist/vendor/codali/permissions/PermissionGate.d.ts.map +1 -0
- package/dist/vendor/codali/permissions/PermissionGate.js +97 -0
- package/dist/vendor/codali/providers/ConfiguredEmbeddings.d.ts +43 -0
- package/dist/vendor/codali/providers/ConfiguredEmbeddings.d.ts.map +1 -0
- package/dist/vendor/codali/providers/ConfiguredEmbeddings.js +87 -0
- package/dist/vendor/codali/providers/ConfiguredModelScope.d.ts +69 -0
- package/dist/vendor/codali/providers/ConfiguredModelScope.d.ts.map +1 -0
- package/dist/vendor/codali/providers/ConfiguredModelScope.js +113 -0
- package/dist/vendor/codali/providers/ConfiguredProvider.d.ts +79 -0
- package/dist/vendor/codali/providers/ConfiguredProvider.d.ts.map +1 -0
- package/dist/vendor/codali/providers/ConfiguredProvider.js +238 -0
- package/dist/vendor/codali/providers/ModelServiceError.d.ts +10 -0
- package/dist/vendor/codali/providers/ModelServiceError.d.ts.map +1 -0
- package/dist/vendor/codali/providers/ModelServiceError.js +45 -0
- package/dist/vendor/codali/providers/MswarmWorkerProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/MswarmWorkerProvider.js +4 -0
- package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/OpenAiCompatibleProvider.js +91 -50
- package/dist/vendor/codali/providers/ProviderTypes.d.ts +12 -0
- package/dist/vendor/codali/providers/ProviderTypes.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/CodaliJobRuntime.d.ts +6 -0
- package/dist/vendor/codali/runtime/CodaliJobRuntime.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/CodaliJobRuntime.js +36 -1
- package/dist/vendor/codali/runtime/CodaliRuntime.d.ts +28 -0
- package/dist/vendor/codali/runtime/CodaliRuntime.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/CodaliRuntime.js +69 -1
- package/dist/vendor/codali/runtime/CompletionReport.d.ts +5 -0
- package/dist/vendor/codali/runtime/CompletionReport.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/CompletionReport.js +12 -0
- package/dist/vendor/codali/runtime/ProgressTracker.d.ts +17 -0
- package/dist/vendor/codali/runtime/ProgressTracker.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/ProgressTracker.js +27 -0
- package/dist/vendor/codali/runtime/RequestedTestEvidence.d.ts +12 -0
- package/dist/vendor/codali/runtime/RequestedTestEvidence.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/RequestedTestEvidence.js +16 -0
- package/dist/vendor/codali/runtime/RequiredRetrieval.d.ts +13 -0
- package/dist/vendor/codali/runtime/RequiredRetrieval.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/RequiredRetrieval.js +25 -0
- package/dist/vendor/codali/runtime/ReviewSource.d.ts +6 -0
- package/dist/vendor/codali/runtime/ReviewSource.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/ReviewSource.js +38 -0
- package/dist/vendor/codali/runtime/Runner.d.ts +7 -0
- package/dist/vendor/codali/runtime/Runner.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/Runner.js +215 -171
- package/dist/vendor/codali/runtime/SecretRedaction.d.ts +11 -0
- package/dist/vendor/codali/runtime/SecretRedaction.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/SecretRedaction.js +85 -0
- package/dist/vendor/codali/runtime/SessionController.d.ts +148 -0
- package/dist/vendor/codali/runtime/SessionController.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/SessionController.js +1356 -0
- package/dist/vendor/codali/runtime/StructuralEvidence.d.ts +22 -0
- package/dist/vendor/codali/runtime/StructuralEvidence.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/StructuralEvidence.js +87 -0
- package/dist/vendor/codali/runtime/TaskReviewContext.d.ts +34 -0
- package/dist/vendor/codali/runtime/TaskReviewContext.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/TaskReviewContext.js +34 -0
- package/dist/vendor/codali/runtime/ValidationOutput.d.ts +5 -0
- package/dist/vendor/codali/runtime/ValidationOutput.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/ValidationOutput.js +32 -0
- package/dist/vendor/codali/runtime/VerificationCommand.d.ts +3 -0
- package/dist/vendor/codali/runtime/VerificationCommand.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/VerificationCommand.js +48 -0
- package/dist/vendor/codali/runtime/WorkStatusPage.d.ts +15 -0
- package/dist/vendor/codali/runtime/WorkStatusPage.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/WorkStatusPage.js +58 -0
- package/dist/vendor/codali/runtime/WorkspaceLedger.d.ts +13 -0
- package/dist/vendor/codali/runtime/WorkspaceLedger.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/WorkspaceLedger.js +145 -0
- package/dist/vendor/codali/runtime/WorkspaceLock.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/WorkspaceLock.js +20 -17
- package/dist/vendor/codali/runtime/WorkspaceSnapshot.d.ts +13 -0
- package/dist/vendor/codali/runtime/WorkspaceSnapshot.d.ts.map +1 -0
- package/dist/vendor/codali/runtime/WorkspaceSnapshot.js +88 -0
- package/dist/vendor/codali/session/AtomicState.d.ts +13 -0
- package/dist/vendor/codali/session/AtomicState.d.ts.map +1 -0
- package/dist/vendor/codali/session/AtomicState.js +109 -0
- package/dist/vendor/codali/session/SessionStoragePath.d.ts +15 -0
- package/dist/vendor/codali/session/SessionStoragePath.d.ts.map +1 -0
- package/dist/vendor/codali/session/SessionStoragePath.js +89 -0
- package/dist/vendor/codali/session/SessionStore.d.ts +9 -0
- package/dist/vendor/codali/session/SessionStore.d.ts.map +1 -1
- package/dist/vendor/codali/session/SessionStore.js +181 -47
- package/dist/vendor/codali/session/WorkState.d.ts +23 -0
- package/dist/vendor/codali/session/WorkState.d.ts.map +1 -0
- package/dist/vendor/codali/session/WorkState.js +367 -0
- package/dist/vendor/codali/session/WorkTypes.d.ts +94 -0
- package/dist/vendor/codali/session/WorkTypes.d.ts.map +1 -0
- package/dist/vendor/codali/session/WorkTypes.js +1 -0
- package/dist/vendor/codali/tools/ToolRegistry.d.ts +2 -0
- package/dist/vendor/codali/tools/ToolRegistry.d.ts.map +1 -1
- package/dist/vendor/codali/tools/ToolRegistry.js +12 -2
- package/dist/vendor/codali/tools/ToolTypes.d.ts +16 -0
- package/dist/vendor/codali/tools/ToolTypes.d.ts.map +1 -1
- package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts +2 -0
- package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts.map +1 -1
- package/dist/vendor/codali/tools/docdex/DocdexTools.js +17 -2
- package/dist/vendor/codali/tools/filesystem/FileTools.d.ts.map +1 -1
- package/dist/vendor/codali/tools/filesystem/FileTools.js +30 -10
- package/dist/vendor/codali/tools/shell/AsyncProcess.d.ts +16 -0
- package/dist/vendor/codali/tools/shell/AsyncProcess.d.ts.map +1 -0
- package/dist/vendor/codali/tools/shell/AsyncProcess.js +94 -0
- package/dist/vendor/codali/tools/shell/DarwinRuntimeLibraries.d.ts +13 -0
- package/dist/vendor/codali/tools/shell/DarwinRuntimeLibraries.d.ts.map +1 -0
- package/dist/vendor/codali/tools/shell/DarwinRuntimeLibraries.js +130 -0
- package/dist/vendor/codali/tools/shell/HostToolchain.d.ts +19 -0
- package/dist/vendor/codali/tools/shell/HostToolchain.d.ts.map +1 -0
- package/dist/vendor/codali/tools/shell/HostToolchain.js +264 -0
- package/dist/vendor/codali/tools/shell/IsolatedExecutor.d.ts +15 -0
- package/dist/vendor/codali/tools/shell/IsolatedExecutor.d.ts.map +1 -0
- package/dist/vendor/codali/tools/shell/IsolatedExecutor.js +134 -0
- package/dist/vendor/codali/tools/shell/ShellTool.d.ts.map +1 -1
- package/dist/vendor/codali/tools/shell/ShellTool.js +9 -29
- package/dist/vendor/codali/tools/shell/ShellWorkingDirectory.d.ts +3 -0
- package/dist/vendor/codali/tools/shell/ShellWorkingDirectory.d.ts.map +1 -0
- package/dist/vendor/codali/tools/shell/ShellWorkingDirectory.js +30 -0
- package/package.json +31 -29
- package/scripts/backend-http-drain.mjs +152 -0
- package/scripts/media_gpu_lock.py +238 -0
- package/scripts/stable-audio-3-openai-server.py +45 -14
- package/scripts/stable-diffusion-cpp-openai-server.py +34 -4
- package/scripts/stable-diffusion-cpp-webm-duration.patch +22 -0
- package/scripts/wan22-openai-server.py +97 -18
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
export const mediaJobSummary = (job) => ({ id: job.id, operation: job.operation, status: job.status, model: job.modelId, configHash: job.configHash, cancelRequested: job.cancelRequested, remote: job.remote, error: job.error, artifact: job.artifact });
|
|
3
|
+
export const createConfiguredMediaTools = (resolved, store) => {
|
|
4
|
+
const properties = { prompt: { type: "string", minLength: 1 }, negative_prompt: { type: "string" }, size: { type: "string" }, seed: { type: "integer" }, steps: { type: "integer", minimum: 1 }, high_noise_steps: { type: "integer", minimum: 1 }, duration_seconds: { type: "number", minimum: 0 }, fps: { type: "integer", minimum: 1 }, video_frames: { type: "integer", minimum: 1 }, sample_rate: { type: "integer", minimum: 1 } };
|
|
5
|
+
const tools = [];
|
|
6
|
+
for (const [binding, operation] of [["image", "images.generations"], ["audio", "audio.generations"], ["video", "videos.generations"]])
|
|
7
|
+
if (resolved.config.bindings[binding]?.enabled !== false && resolved.config.bindings[binding])
|
|
8
|
+
tools.push({
|
|
9
|
+
name: `media_generate_${binding}`, description: `Submit a ${binding} generation job using the approved model configuration. Returns a durable job ID; use media_job_wait to obtain its artifact. Never resubmit uncertain jobs.`, capability: "media", readOnly: false,
|
|
10
|
+
inputSchema: { type: "object", properties, required: ["prompt"], additionalProperties: false },
|
|
11
|
+
handler: async (args, context) => { const data = mediaJobSummary(await store().submit(resolved, binding, operation, args, context.signal)); return { output: JSON.stringify(data), data }; },
|
|
12
|
+
});
|
|
13
|
+
for (const action of ["status", "wait", "cancel"])
|
|
14
|
+
tools.push({
|
|
15
|
+
name: `media_job_${action}`, description: `${action} a durable media job. Wait suspends locally without additional model calls and reports progress; cancelling a wait leaves the render running. An uncertain remote outcome must be reconciled before another submission.`, capability: "media", readOnly: action !== "cancel", inputSchema: { type: "object", properties: { jobId: { type: "string" }, ...(action === "wait" ? { waitSeconds: { type: "integer", minimum: 1, maximum: 600 } } : {}) }, required: ["jobId"], additionalProperties: false },
|
|
16
|
+
handler: async (args, context) => {
|
|
17
|
+
const id = args.jobId;
|
|
18
|
+
if (action === "cancel") {
|
|
19
|
+
const data = mediaJobSummary(await store().cancel(id));
|
|
20
|
+
return { output: JSON.stringify(data), data };
|
|
21
|
+
}
|
|
22
|
+
let job = await store().reconcileProcess(id, context.signal);
|
|
23
|
+
const waitSeconds = args.waitSeconds ?? 300;
|
|
24
|
+
const until = Date.now() + waitSeconds * 1000;
|
|
25
|
+
let lastProgress = 0;
|
|
26
|
+
while (action === "wait" && ["queued", "running"].includes(job.status) && Date.now() < until) {
|
|
27
|
+
context.signal?.throwIfAborted();
|
|
28
|
+
if (Date.now() - lastProgress >= 5000) {
|
|
29
|
+
context.onOutput?.("stdout", `Media ${id}: ${job.status}\n`);
|
|
30
|
+
lastProgress = Date.now();
|
|
31
|
+
}
|
|
32
|
+
await delay(Math.min(500, Math.max(1, until - Date.now())), undefined, { signal: context.signal });
|
|
33
|
+
job = await store().reconcileProcess(id, context.signal);
|
|
34
|
+
}
|
|
35
|
+
const data = mediaJobSummary(job);
|
|
36
|
+
return { output: JSON.stringify(data), data };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
tools.push({ name: "media_export_artifact", description: "Copy a completed, checksum-verified media artifact to a new workspace file. Requires workspace write permission and a claimed task scope.", capability: "workspace", readOnly: false, inputSchema: { type: "object", properties: { jobId: { type: "string" }, path: { type: "string" } }, required: ["jobId", "path"], additionalProperties: false }, handler: async (args, context) => { const { jobId, path } = args; const data = await store().exportArtifact(jobId, context.workspaceRoot, path, resolvedPath => context.checkPath?.(resolvedPath, "write") ?? Promise.resolve()); context.recordTouchedFile?.(path); return { output: JSON.stringify(data), data }; } });
|
|
40
|
+
return tools;
|
|
41
|
+
};
|
|
@@ -31,9 +31,9 @@ export const createImageGenerationTool = (options) => {
|
|
|
31
31
|
name: IMAGE_TOOL_NAME,
|
|
32
32
|
description: "Generate an image from a text prompt. Returns the path to the written image file, not the image itself. Use for any request to draw, render, or generate a picture.",
|
|
33
33
|
capability: "media",
|
|
34
|
-
// Generation
|
|
35
|
-
//
|
|
36
|
-
readOnly:
|
|
34
|
+
// Generation consumes compute and writes an artifact, so it requires
|
|
35
|
+
// explicit media permission even when the artifact directory is private.
|
|
36
|
+
readOnly: false,
|
|
37
37
|
inputSchema: {
|
|
38
38
|
type: "object",
|
|
39
39
|
required: ["prompt"],
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MediaArtifact, MediaJob } from "./MediaJobStore.js";
|
|
2
|
+
/** Persist received output before acknowledging success; never resubmit inference during recovery. */
|
|
3
|
+
export declare const persistMediaArtifact: (directory: string, job: MediaJob, bytes: Buffer) => Promise<MediaArtifact>;
|
|
4
|
+
export declare const recoverMediaArtifact: (directory: string, job: MediaJob) => Promise<MediaArtifact | undefined>;
|
|
5
|
+
//# sourceMappingURL=MediaArtifactReceipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaArtifactReceipt.d.ts","sourceRoot":"","sources":["../../../src/connectors/media/MediaArtifactReceipt.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAUlE,sGAAsG;AACtG,eAAO,MAAM,oBAAoB,GAAU,WAAW,MAAM,EAAE,KAAK,QAAQ,EAAE,OAAO,MAAM,KAAG,OAAO,CAAC,aAAa,CAejH,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAU,WAAW,MAAM,EAAE,KAAK,QAAQ,KAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAY9G,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { atomicWriteJson, syncStateDirectory } from "../../session/AtomicState.js";
|
|
5
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
6
|
+
const format = (job) => job.operation === "images.generations" ? { extension: "png", mimeType: "image/png" }
|
|
7
|
+
: job.operation === "audio.generations" ? { extension: "wav", mimeType: "audio/wav" }
|
|
8
|
+
: job.operation === "videos.generations" ? { extension: "webm", mimeType: "video/webm" } : undefined;
|
|
9
|
+
const matchesFormat = (bytes, extension) => extension === "png" ? bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
|
|
10
|
+
: extension === "wav" ? bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WAVE"
|
|
11
|
+
: bytes.subarray(0, 4).equals(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
|
|
12
|
+
/** Persist received output before acknowledging success; never resubmit inference during recovery. */
|
|
13
|
+
export const persistMediaArtifact = async (directory, job, bytes) => {
|
|
14
|
+
const media = format(job);
|
|
15
|
+
if (!media || bytes.length > 67108864 || !matchesFormat(bytes, media.extension))
|
|
16
|
+
throw new Error("Media file format or size does not match the advertised contract");
|
|
17
|
+
const artifact = { id: `${job.id}-artifact`, jobId: job.id, path: path.join(directory, `artifact.${media.extension}`), mimeType: media.mimeType,
|
|
18
|
+
bytes: bytes.length, sha256: hash(bytes), model: job.modelId, configHash: job.configHash, createdAt: new Date().toISOString() };
|
|
19
|
+
await atomicWriteJson(path.join(directory, "artifact-receipt.json"), { schemaVersion: 1, sessionId: job.sessionId, artifact });
|
|
20
|
+
const temporary = path.join(directory, `.artifact-${randomUUID()}.partial`);
|
|
21
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
22
|
+
try {
|
|
23
|
+
await handle.writeFile(bytes);
|
|
24
|
+
await handle.sync();
|
|
25
|
+
await handle.close();
|
|
26
|
+
// link is atomic and fails if the destination exists; a partially written artifact is never published.
|
|
27
|
+
await fs.link(temporary, artifact.path);
|
|
28
|
+
await syncStateDirectory(directory);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await handle.close().catch(() => undefined);
|
|
32
|
+
await fs.unlink(temporary).catch(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
return artifact;
|
|
35
|
+
};
|
|
36
|
+
export const recoverMediaArtifact = async (directory, job) => {
|
|
37
|
+
let receipt;
|
|
38
|
+
try {
|
|
39
|
+
receipt = JSON.parse(await fs.readFile(path.join(directory, "artifact-receipt.json"), "utf8"));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code === "ENOENT")
|
|
43
|
+
return undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const media = format(job);
|
|
47
|
+
const artifact = receipt.artifact;
|
|
48
|
+
if (!media || receipt.schemaVersion !== 1 || receipt.sessionId !== job.sessionId || artifact.jobId !== job.id || artifact.id !== `${job.id}-artifact` || artifact.configHash !== job.configHash || artifact.model !== job.modelId || artifact.mimeType !== media.mimeType || artifact.path !== path.join(directory, `artifact.${media.extension}`))
|
|
49
|
+
throw new Error("Media artifact receipt does not match this job");
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = await fs.lstat(artifact.path);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return undefined;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (!stat.isFile() || stat.size !== artifact.bytes || stat.size > 67108864)
|
|
60
|
+
throw new Error("Media artifact receipt size or path is invalid");
|
|
61
|
+
const bytes = await fs.readFile(artifact.path);
|
|
62
|
+
if (hash(bytes) !== artifact.sha256 || !matchesFormat(bytes, media.extension))
|
|
63
|
+
throw new Error("Media artifact receipt checksum or format mismatch");
|
|
64
|
+
return artifact;
|
|
65
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ModelOperation, type ResolvedOrchestrationConfig } from "../../config/OrchestrationConfig.js";
|
|
2
|
+
export interface MediaArtifact {
|
|
3
|
+
id: string;
|
|
4
|
+
jobId: string;
|
|
5
|
+
path: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
bytes: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
model: string;
|
|
10
|
+
configHash: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MediaJob {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
id: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
operation: ModelOperation;
|
|
18
|
+
binding: string;
|
|
19
|
+
modelId: string;
|
|
20
|
+
catalogId: string;
|
|
21
|
+
providerId: string;
|
|
22
|
+
configHash: string;
|
|
23
|
+
resolved: ResolvedOrchestrationConfig;
|
|
24
|
+
request: Record<string, unknown>;
|
|
25
|
+
status: "queued" | "running" | "succeeded" | "failed" | "cancelled" | "uncertain";
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
pid?: number;
|
|
29
|
+
cancelRequested?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
artifact?: MediaArtifact;
|
|
32
|
+
reconciliation?: {
|
|
33
|
+
at: string;
|
|
34
|
+
outcome: "failed";
|
|
35
|
+
note: string;
|
|
36
|
+
};
|
|
37
|
+
remote?: {
|
|
38
|
+
protocol: "mswarm-media-jobs-v1";
|
|
39
|
+
submissionStartedAt: string;
|
|
40
|
+
jobId?: string;
|
|
41
|
+
invocationId?: string;
|
|
42
|
+
status?: string;
|
|
43
|
+
nodeId?: string;
|
|
44
|
+
expiresAt?: string;
|
|
45
|
+
resultSha256?: string;
|
|
46
|
+
acknowledgedAt?: string;
|
|
47
|
+
cancelRequested?: boolean;
|
|
48
|
+
cancelConfirmed?: boolean;
|
|
49
|
+
usage?: Record<string, unknown>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare class MediaJobStore {
|
|
53
|
+
readonly root: string;
|
|
54
|
+
readonly sessionId: string;
|
|
55
|
+
constructor(root: string, sessionId: string);
|
|
56
|
+
file(id: string): string;
|
|
57
|
+
read(id: string): Promise<MediaJob>;
|
|
58
|
+
update(id: string, mutate: (job: MediaJob) => void): Promise<MediaJob>;
|
|
59
|
+
list(): Promise<MediaJob[]>;
|
|
60
|
+
submit(resolved: ResolvedOrchestrationConfig, binding: string, operation: ModelOperation, request: Record<string, unknown>, signal?: AbortSignal): Promise<MediaJob>;
|
|
61
|
+
cancel(id: string): Promise<MediaJob>;
|
|
62
|
+
/** Trusted user/host action, deliberately unavailable as an agent tool. */
|
|
63
|
+
reconcileFailure(id: string, note: string): Promise<MediaJob>;
|
|
64
|
+
reconcileProcess(id: string, signal?: AbortSignal): Promise<MediaJob>;
|
|
65
|
+
exportArtifact(id: string, workspaceRoot: string, destination: string, checkPath?: (resolvedPath: string) => Promise<void>): Promise<MediaArtifact>;
|
|
66
|
+
}
|
|
67
|
+
export declare const validateMediaRequest: (request: Record<string, unknown>, descriptor: Record<string, unknown>, operation: ModelOperation) => Record<string, unknown>;
|
|
68
|
+
//# sourceMappingURL=MediaJobStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaJobStore.d.ts","sourceRoot":"","sources":["../../../src/connectors/media/MediaJobStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { atomicWriteJson, tryStateLock, withStateLock } from "../../session/AtomicState.js";
|
|
7
|
+
import { pinOrchestrationConfig } from "../../config/OrchestrationConfig.js";
|
|
8
|
+
import { ConfiguredProvider } from "../../providers/ConfiguredProvider.js";
|
|
9
|
+
import { recoverMediaArtifact } from "./MediaArtifactReceipt.js";
|
|
10
|
+
import { reconcileRemoteMediaJob } from "./RemoteMediaJob.js";
|
|
11
|
+
const validId = (id) => { if (!/^[a-zA-Z0-9-]{1,120}$/.test(id))
|
|
12
|
+
throw new Error("Invalid media job id"); return id; };
|
|
13
|
+
export class MediaJobStore {
|
|
14
|
+
constructor(root, sessionId) {
|
|
15
|
+
this.root = root;
|
|
16
|
+
this.sessionId = sessionId;
|
|
17
|
+
validId(sessionId);
|
|
18
|
+
}
|
|
19
|
+
file(id) { return path.join(this.root, validId(id), "job.json"); }
|
|
20
|
+
async read(id) {
|
|
21
|
+
const job = JSON.parse(await fs.readFile(this.file(id), "utf8"));
|
|
22
|
+
if (job.schemaVersion !== 1 || job.sessionId !== this.sessionId || job.id !== id)
|
|
23
|
+
throw new Error("Media job belongs to another session");
|
|
24
|
+
return job;
|
|
25
|
+
}
|
|
26
|
+
async update(id, mutate) {
|
|
27
|
+
return withStateLock(this.file(id), async () => { const job = await this.read(id); mutate(job); job.updatedAt = new Date().toISOString(); await atomicWriteJson(this.file(id), job); return job; });
|
|
28
|
+
}
|
|
29
|
+
async list() {
|
|
30
|
+
let names;
|
|
31
|
+
try {
|
|
32
|
+
names = await fs.readdir(this.root);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === "ENOENT")
|
|
36
|
+
return [];
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const jobs = [];
|
|
40
|
+
for (const id of names) {
|
|
41
|
+
if (!/^[a-zA-Z0-9-]{1,120}$/.test(id))
|
|
42
|
+
continue;
|
|
43
|
+
const job = await this.read(id);
|
|
44
|
+
jobs.push(job);
|
|
45
|
+
}
|
|
46
|
+
return jobs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
47
|
+
}
|
|
48
|
+
async submit(resolved, binding, operation, request, signal) {
|
|
49
|
+
resolved = pinOrchestrationConfig(resolved);
|
|
50
|
+
if ((await this.list()).some(job => ["queued", "running", "uncertain"].includes(job.status)))
|
|
51
|
+
throw new Error("Resolve the active or uncertain media job before submitting another render in this session");
|
|
52
|
+
const selected = await new ConfiguredProvider(resolved, binding).select(operation, signal);
|
|
53
|
+
const descriptor = selected.actual.raw.operations?.find(v => v.type === operation);
|
|
54
|
+
if (!descriptor)
|
|
55
|
+
throw new Error("The service did not advertise this media operation contract");
|
|
56
|
+
const body = validateMediaRequest(request, descriptor, operation);
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const at = new Date().toISOString();
|
|
59
|
+
const id = randomUUID();
|
|
60
|
+
const job = { schemaVersion: 1, id, sessionId: this.sessionId, operation, binding, modelId: selected.id, catalogId: selected.model.catalogId, providerId: selected.model.provider, configHash: resolved.hash, resolved, request: { ...body, model: selected.model.catalogId }, status: "queued", createdAt: at, updatedAt: at };
|
|
61
|
+
await withStateLock(path.join(this.root, ".submission"), async () => {
|
|
62
|
+
if ((await this.list()).some(existing => ["queued", "running", "uncertain"].includes(existing.status)))
|
|
63
|
+
throw new Error("Resolve the active or uncertain media job before submitting another render in this session");
|
|
64
|
+
await atomicWriteJson(this.file(id), job);
|
|
65
|
+
});
|
|
66
|
+
const credentialName = selected.provider.credentialRef?.slice(4);
|
|
67
|
+
const env = { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG };
|
|
68
|
+
if (credentialName && process.env[credentialName])
|
|
69
|
+
env[credentialName] = process.env[credentialName];
|
|
70
|
+
try {
|
|
71
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL("./MediaJobWorker.js", import.meta.url)), this.root, this.sessionId, id], { detached: true, stdio: "ignore", env });
|
|
72
|
+
child.unref();
|
|
73
|
+
child.once("error", () => { void this.update(id, state => { state.status = "failed"; state.error = "Media worker could not start"; }).catch(() => undefined); });
|
|
74
|
+
await this.update(id, state => { state.pid = child.pid; });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
await this.update(id, state => { state.status = "failed"; state.error = "Media worker could not start"; });
|
|
78
|
+
}
|
|
79
|
+
return this.read(id);
|
|
80
|
+
}
|
|
81
|
+
async cancel(id) {
|
|
82
|
+
return this.update(id, job => { if (!["succeeded", "failed", "cancelled"].includes(job.status))
|
|
83
|
+
job.cancelRequested = true; });
|
|
84
|
+
}
|
|
85
|
+
/** Trusted user/host action, deliberately unavailable as an agent tool. */
|
|
86
|
+
async reconcileFailure(id, note) {
|
|
87
|
+
if (!note.trim() || note.length > 4000)
|
|
88
|
+
throw new Error("Describe the confirmed remote failure or cancellation (1–4000 characters)");
|
|
89
|
+
return this.update(id, job => {
|
|
90
|
+
if (job.status !== "uncertain")
|
|
91
|
+
throw new Error("Only an uncertain media job needs manual reconciliation");
|
|
92
|
+
job.status = "failed";
|
|
93
|
+
job.reconciliation = { at: new Date().toISOString(), outcome: "failed", note };
|
|
94
|
+
job.error = "Remote failure/cancellation confirmed by user; see reconciliation record";
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async reconcileProcess(id, signal) {
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
const job = await this.read(id);
|
|
100
|
+
if (["succeeded", "failed", "cancelled"].includes(job.status) || job.status === "queued" && Date.now() - Date.parse(job.createdAt) <= 60000)
|
|
101
|
+
return job;
|
|
102
|
+
const recovery = await tryStateLock(`${this.file(id)}.execution`, async () => {
|
|
103
|
+
const current = await this.read(id);
|
|
104
|
+
if (["succeeded", "failed", "cancelled"].includes(current.status))
|
|
105
|
+
return current;
|
|
106
|
+
let artifact;
|
|
107
|
+
let failure;
|
|
108
|
+
try {
|
|
109
|
+
artifact = await recoverMediaArtifact(path.dirname(this.file(id)), current);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
failure = error instanceof Error ? error.message : "Artifact recovery failed";
|
|
113
|
+
}
|
|
114
|
+
if (!artifact && !failure && current.remote) {
|
|
115
|
+
try {
|
|
116
|
+
return await reconcileRemoteMediaJob(this, current, signal);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
signal?.throwIfAborted();
|
|
120
|
+
failure = error instanceof Error ? error.message : "Remote media recovery failed";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return this.update(id, state => {
|
|
124
|
+
if (["succeeded", "failed", "cancelled"].includes(state.status))
|
|
125
|
+
return;
|
|
126
|
+
if (artifact) {
|
|
127
|
+
state.status = "succeeded";
|
|
128
|
+
state.artifact = artifact;
|
|
129
|
+
state.error = undefined;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
state.status = "uncertain";
|
|
133
|
+
state.error = failure ?? "Media worker stopped without a complete local receipt; remote execution may have occurred. Do not resubmit automatically.";
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
return recovery.acquired ? recovery.value : job;
|
|
138
|
+
}
|
|
139
|
+
async exportArtifact(id, workspaceRoot, destination, checkPath) {
|
|
140
|
+
const job = await this.read(id);
|
|
141
|
+
const artifact = job.artifact;
|
|
142
|
+
if (job.status !== "succeeded" || !artifact)
|
|
143
|
+
throw new Error("Media artifact is not ready");
|
|
144
|
+
const target = path.resolve(workspaceRoot, destination);
|
|
145
|
+
const root = await fs.realpath(workspaceRoot);
|
|
146
|
+
const parent = await fs.realpath(path.dirname(target));
|
|
147
|
+
if (parent !== root && !parent.startsWith(`${root}${path.sep}`))
|
|
148
|
+
throw new Error("Artifact destination escapes the workspace");
|
|
149
|
+
const canonicalTarget = path.join(parent, path.basename(target));
|
|
150
|
+
if ([".git", ".mcoda"].includes(path.relative(root, canonicalTarget).split(path.sep)[0]))
|
|
151
|
+
throw new Error("Artifact cannot overwrite repository control metadata");
|
|
152
|
+
await checkPath?.(canonicalTarget);
|
|
153
|
+
const bytes = await fs.readFile(artifact.path);
|
|
154
|
+
if (createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
|
155
|
+
throw new Error("Artifact checksum mismatch");
|
|
156
|
+
await fs.writeFile(canonicalTarget, bytes, { flag: "wx", mode: 0o600 });
|
|
157
|
+
return { ...artifact, path: canonicalTarget };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export const validateMediaRequest = (request, descriptor, operation) => {
|
|
161
|
+
const allowlist = descriptor.supported_parameters;
|
|
162
|
+
if (!Array.isArray(allowlist))
|
|
163
|
+
throw new Error("Missing advertised media parameters");
|
|
164
|
+
for (const name of Object.keys(request))
|
|
165
|
+
if (!allowlist.includes(name) || ["model", "response_format", "n"].includes(name))
|
|
166
|
+
throw new Error(`Unsupported media parameter: ${name}`);
|
|
167
|
+
const limits = (descriptor.limits ?? {});
|
|
168
|
+
if (typeof request.prompt !== "string" || !request.prompt.trim() || request.prompt.length > (limits.max_prompt_chars ?? 8192))
|
|
169
|
+
throw new Error("Invalid media prompt length");
|
|
170
|
+
const body = { ...request, n: 1, response_format: operation === "images.generations" ? "b64_json" : operation === "audio.generations" ? "wav" : "webm" };
|
|
171
|
+
if (!descriptor.response_formats?.includes(body.response_format))
|
|
172
|
+
throw new Error("Required media output format is not supported");
|
|
173
|
+
for (const name of ["steps", "high_noise_steps", "duration_seconds", "sample_rate", "fps", "video_frames"]) {
|
|
174
|
+
const value = request[name];
|
|
175
|
+
if (value === undefined)
|
|
176
|
+
continue;
|
|
177
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || (name !== "duration_seconds" && !Number.isInteger(value)))
|
|
178
|
+
throw new Error(`Invalid media ${name}`);
|
|
179
|
+
if (limits[`min_${name}`] !== undefined && value < limits[`min_${name}`] || limits[`max_${name}`] !== undefined && value > limits[`max_${name}`])
|
|
180
|
+
throw new Error(`Media ${name} exceeds advertised limits`);
|
|
181
|
+
}
|
|
182
|
+
if (request.size !== undefined) {
|
|
183
|
+
if (typeof request.size !== "string" || !/^\d+x\d+$/.test(request.size))
|
|
184
|
+
throw new Error("Invalid media size");
|
|
185
|
+
const [width, height] = request.size.split("x").map(Number);
|
|
186
|
+
for (const [name, value] of [["width", width], ["height", height]])
|
|
187
|
+
if (value < (limits[`min_${name}`] ?? 1) || value > (limits[`max_${name}`] ?? 4096))
|
|
188
|
+
throw new Error(`Media ${name} exceeds advertised limits`);
|
|
189
|
+
if (width * height > (limits.max_pixels ?? 16777216))
|
|
190
|
+
throw new Error("Media pixel count exceeds advertised limit");
|
|
191
|
+
}
|
|
192
|
+
if (typeof request.negative_prompt === "string" && request.negative_prompt.length > (limits.max_negative_prompt_chars ?? 8192))
|
|
193
|
+
throw new Error("Negative prompt too long");
|
|
194
|
+
if (request.seed !== undefined && (typeof request.seed !== "number" || !Number.isSafeInteger(request.seed)))
|
|
195
|
+
throw new Error("Invalid seed");
|
|
196
|
+
if (Buffer.byteLength(JSON.stringify(body)) > (limits.max_request_bytes ?? 131072))
|
|
197
|
+
throw new Error("Media request exceeds byte limit");
|
|
198
|
+
return body;
|
|
199
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaJobWorker.d.ts","sourceRoot":"","sources":["../../../src/connectors/media/MediaJobWorker.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,EAAE,WAAW,MAAM,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,IAAI,CA+B/F,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ModelServiceClient } from "../../providers/ConfiguredProvider.js";
|
|
4
|
+
import { ModelServiceError } from "../../providers/ModelServiceError.js";
|
|
5
|
+
import { MediaJobStore } from "./MediaJobStore.js";
|
|
6
|
+
import { withStateLock } from "../../session/AtomicState.js";
|
|
7
|
+
import { persistMediaArtifact } from "./MediaArtifactReceipt.js";
|
|
8
|
+
import { pinOrchestrationConfig, resolveBinding } from "../../config/OrchestrationConfig.js";
|
|
9
|
+
import { executeRemoteMediaJob } from "./RemoteMediaJob.js";
|
|
10
|
+
export const executeMediaJob = async (root, sessionId, id) => {
|
|
11
|
+
const store = new MediaJobStore(root, sessionId);
|
|
12
|
+
// A worker crash releases no success claim and never automatically resubmits.
|
|
13
|
+
await withStateLock(`${store.file(id)}.execution`, async () => {
|
|
14
|
+
const job = await store.read(id);
|
|
15
|
+
if (job.status !== "queued")
|
|
16
|
+
return;
|
|
17
|
+
if (job.cancelRequested) {
|
|
18
|
+
await store.update(id, state => { state.status = "failed"; state.error = "Cancelled before submission"; });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
job.resolved = pinOrchestrationConfig(job.resolved);
|
|
23
|
+
const model = job.resolved.config.models[job.modelId];
|
|
24
|
+
if (job.configHash !== job.resolved.hash || model?.provider !== job.providerId || model?.catalogId !== job.catalogId || job.request.model !== job.catalogId || !resolveBinding(job.resolved.config, job.binding, job.operation).some(candidate => candidate.id === job.modelId))
|
|
25
|
+
throw new Error("Media job route does not match its pinned configuration");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
await store.update(id, state => { state.status = "failed"; state.error = error instanceof Error ? error.message : "Invalid media configuration"; });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await store.update(id, state => { state.status = "running"; state.pid = process.pid; });
|
|
32
|
+
const abort = new AbortController();
|
|
33
|
+
let checking = false;
|
|
34
|
+
const durable = !!job.resolved.config.providers[job.providerId].mediaJobsUrl;
|
|
35
|
+
const timer = setInterval(() => { if (checking || durable)
|
|
36
|
+
return; checking = true; void store.read(id).then(state => { if (state.cancelRequested)
|
|
37
|
+
abort.abort(new Error("Cancellation requested; remote termination is unconfirmed")); }).catch(error => abort.abort(error)).finally(() => { checking = false; }); }, 500);
|
|
38
|
+
try {
|
|
39
|
+
const provider = job.resolved.config.providers[job.providerId];
|
|
40
|
+
if (durable) {
|
|
41
|
+
await executeRemoteMediaJob(store, job);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const endpoint = `${provider.baseUrl.replace(/\/$/, "")}/${job.operation.replace(".", "/")}`;
|
|
45
|
+
const response = await new ModelServiceClient(provider).json(endpoint, { method: "POST", body: job.request, signal: abort.signal, timeoutMs: 60 * 60 * 1000 });
|
|
46
|
+
const payload = response.data?.[0];
|
|
47
|
+
const base64 = payload?.b64_json ?? payload?.b64_audio ?? payload?.b64_video;
|
|
48
|
+
if (typeof base64 !== "string" || !base64 || base64.length > 89478488 || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64))
|
|
49
|
+
throw new Error("Invalid or oversized media payload");
|
|
50
|
+
const bytes = Buffer.from(base64, "base64");
|
|
51
|
+
const artifact = await persistMediaArtifact(path.dirname(store.file(id)), job, bytes);
|
|
52
|
+
await store.update(id, state => { state.status = "succeeded"; state.artifact = artifact; });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const rejected = error instanceof ModelServiceError && [400, 401, 402, 403, 404, 405, 413, 415, 422, 429].includes(error.status);
|
|
56
|
+
await store.update(id, state => { state.status = rejected && !state.remote ? "failed" : "uncertain"; state.error = error instanceof Error ? error.message : "Media submission outcome is unknown"; });
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearInterval(timer);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
64
|
+
executeMediaJob(process.argv[2], process.argv[3], process.argv[4]).catch(() => { process.exitCode = 1; });
|
|
65
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MediaJob, MediaJobStore } from "./MediaJobStore.js";
|
|
2
|
+
/** This path only reads/cancels/acknowledges a known client job id. It never
|
|
3
|
+
* submits again, including after a lost admission response or process crash. */
|
|
4
|
+
export declare const reconcileRemoteMediaJob: (store: MediaJobStore, job: MediaJob, signal?: AbortSignal) => Promise<MediaJob>;
|
|
5
|
+
export declare const executeRemoteMediaJob: (store: MediaJobStore, job: MediaJob) => Promise<void>;
|
|
6
|
+
//# sourceMappingURL=RemoteMediaJob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteMediaJob.d.ts","sourceRoot":"","sources":["../../../src/connectors/media/RemoteMediaJob.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA2BlE;gFACgF;AAChF,eAAO,MAAM,uBAAuB,GAAU,OAAO,aAAa,EAAE,KAAK,QAAQ,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,QAAQ,CA2BzH,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAU,OAAO,aAAa,EAAE,KAAK,QAAQ,KAAG,OAAO,CAAC,IAAI,CAgB7F,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { ModelServiceClient } from "../../providers/ConfiguredProvider.js";
|
|
5
|
+
import { pinOrchestrationConfig, resolveBinding } from "../../config/OrchestrationConfig.js";
|
|
6
|
+
import { persistMediaArtifact } from "./MediaArtifactReceipt.js";
|
|
7
|
+
const connection = (job) => {
|
|
8
|
+
const resolved = pinOrchestrationConfig(job.resolved);
|
|
9
|
+
const provider = resolved.config.providers[job.providerId];
|
|
10
|
+
const model = resolved.config.models[job.modelId];
|
|
11
|
+
if (!provider?.mediaJobsUrl || resolved.hash !== job.configHash || job.request.model !== job.catalogId || model?.provider !== job.providerId || model?.catalogId !== job.catalogId || !resolveBinding(resolved.config, job.binding, job.operation).some(candidate => candidate.id === job.modelId))
|
|
12
|
+
throw new Error("Remote media job does not match its pinned configuration");
|
|
13
|
+
return { client: new ModelServiceClient(provider), url: `${provider.mediaJobsUrl.replace(/\/$/, "")}/${job.id}` };
|
|
14
|
+
};
|
|
15
|
+
const receipt = (job, value) => {
|
|
16
|
+
const v = value;
|
|
17
|
+
if (!v || v.protocol !== "mswarm-media-jobs-v1" || v.client_id !== job.id || typeof v.job_id !== "string" || !/^shj_media_[a-f0-9]{64}$/.test(v.job_id) || v.operation !== job.operation || v.model !== job.catalogId || !["admitting", "queued", "running", "succeeded", "failed", "cancelled", "uncertain", "expired"].includes(v.status))
|
|
18
|
+
throw new Error("Remote media receipt identity or contract mismatch");
|
|
19
|
+
if (job.remote?.jobId && job.remote.jobId !== v.job_id)
|
|
20
|
+
throw new Error("Remote media job identity changed");
|
|
21
|
+
if (v.status === "cancelled" && v.cancel_confirmed !== true)
|
|
22
|
+
throw new Error("The service has not confirmed media cancellation");
|
|
23
|
+
return v;
|
|
24
|
+
};
|
|
25
|
+
const saveReceipt = (store, job, remote) => store.update(job.id, state => {
|
|
26
|
+
if (!state.remote)
|
|
27
|
+
throw new Error("Missing remote submission intent");
|
|
28
|
+
Object.assign(state.remote, { jobId: remote.job_id, invocationId: remote.invocation_id ?? undefined, status: remote.status, nodeId: remote.node_id ?? undefined, expiresAt: remote.expires_at ?? undefined, resultSha256: remote.result_sha256, cancelRequested: remote.cancel_requested === true, cancelConfirmed: remote.cancel_confirmed === true, usage: remote.usage });
|
|
29
|
+
});
|
|
30
|
+
/** This path only reads/cancels/acknowledges a known client job id. It never
|
|
31
|
+
* submits again, including after a lost admission response or process crash. */
|
|
32
|
+
export const reconcileRemoteMediaJob = async (store, job, signal) => {
|
|
33
|
+
const { client, url } = connection(job);
|
|
34
|
+
if (job.cancelRequested && !job.remote?.cancelRequested) {
|
|
35
|
+
const cancelled = receipt(job, await client.json(url, { method: "DELETE", signal }));
|
|
36
|
+
job = await saveReceipt(store, job, cancelled);
|
|
37
|
+
}
|
|
38
|
+
const remote = receipt(job, await client.json(`${url}?result=1`, { signal }));
|
|
39
|
+
job = await saveReceipt(store, job, remote);
|
|
40
|
+
if (remote.status === "succeeded") {
|
|
41
|
+
if (!remote.result || typeof remote.result_sha256 !== "string" || createHash("sha256").update(JSON.stringify(remote.result)).digest("hex") !== remote.result_sha256)
|
|
42
|
+
throw new Error("Remote media result checksum mismatch");
|
|
43
|
+
const data = remote.result.data?.[0];
|
|
44
|
+
const base64 = data?.b64_json ?? data?.b64_audio ?? data?.b64_video;
|
|
45
|
+
if (typeof base64 !== "string" || !base64 || base64.length > 89478488 || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64))
|
|
46
|
+
throw new Error("Invalid or oversized media payload");
|
|
47
|
+
const artifact = await persistMediaArtifact(path.dirname(store.file(job.id)), job, Buffer.from(base64, "base64"));
|
|
48
|
+
// A local durable artifact precedes server acknowledgement. If ACK fails,
|
|
49
|
+
// the successful local receipt remains usable and the server TTL bounds it.
|
|
50
|
+
job = await store.update(job.id, current => { current.status = "succeeded"; current.artifact = artifact; current.error = undefined; });
|
|
51
|
+
try {
|
|
52
|
+
await client.json(`${url}/ack`, { method: "POST", body: { result_sha256: remote.result_sha256 }, signal });
|
|
53
|
+
job = await store.update(job.id, current => { current.remote.acknowledgedAt = new Date().toISOString(); });
|
|
54
|
+
}
|
|
55
|
+
catch { /* Do not downgrade a verified artifact after a lost ACK. */ }
|
|
56
|
+
return job;
|
|
57
|
+
}
|
|
58
|
+
return store.update(job.id, current => {
|
|
59
|
+
current.status = remote.status === "cancelled" ? "cancelled" : ["failed", "expired"].includes(remote.status) ? "failed" : remote.status === "uncertain" ? "uncertain" : "running";
|
|
60
|
+
current.error = remote.error ?? (remote.status === "uncertain" ? "Remote admission or execution is unresolved" : undefined);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
export const executeRemoteMediaJob = async (store, job) => {
|
|
64
|
+
const { client, url } = connection(job);
|
|
65
|
+
if (!job.remote) {
|
|
66
|
+
// Persist before the first network byte. Recovery always uses GET, even
|
|
67
|
+
// when the process dies before it can store the server acknowledgement.
|
|
68
|
+
job = await store.update(job.id, current => { current.remote = { protocol: "mswarm-media-jobs-v1", submissionStartedAt: new Date().toISOString() }; });
|
|
69
|
+
const accepted = receipt(job, await client.json(url, { method: "PUT", body: { operation: job.operation, request: job.request } }));
|
|
70
|
+
job = await saveReceipt(store, job, accepted);
|
|
71
|
+
}
|
|
72
|
+
const deadline = Date.parse(job.remote.submissionStartedAt) + 60 * 60000;
|
|
73
|
+
while (Date.now() < deadline) {
|
|
74
|
+
job = await reconcileRemoteMediaJob(store, await store.read(job.id));
|
|
75
|
+
if (["succeeded", "failed", "cancelled", "uncertain"].includes(job.status))
|
|
76
|
+
return;
|
|
77
|
+
await delay(2000);
|
|
78
|
+
}
|
|
79
|
+
await store.update(job.id, current => { current.status = "uncertain"; current.error = "Media observation deadline elapsed; inspect the original remote job before further work"; });
|
|
80
|
+
};
|